Metadata-Version: 2.4
Name: omnilogin-python-sdk
Version: 0.1.1
Summary: OmniLogin Automation SDK for Python: manage browser profiles and automate with humanized interactions
Project-URL: Homepage, https://github.com/omnilogin-app/omnilogin-python-sdk#readme
Project-URL: Repository, https://github.com/omnilogin-app/omnilogin-python-sdk
Project-URL: Issues, https://github.com/omnilogin-app/omnilogin-python-sdk/issues
Author-email: HiveSoft LTD <support@omnilogin.net>
License-Expression: MIT
Keywords: anti-detect,browser-automation,multilogin,omnilogin,playwright,puppeteer
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
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 :: Browsers
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.5
Requires-Dist: websockets>=12
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: coverage[toml]>=7.5; extra == 'dev'
Requires-Dist: mypy>=1.10; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: twine>=5.1; extra == 'dev'
Description-Content-Type: text/markdown

# omnilogin-python-sdk

[![PyPI version](https://img.shields.io/pypi/v/omnilogin-python-sdk.svg)](https://pypi.org/project/omnilogin-python-sdk/)
[![license](https://img.shields.io/pypi/l/omnilogin-python-sdk.svg)](https://pypi.org/project/omnilogin-python-sdk/)
[![CI/CD](https://github.com/omnilogin-app/omnilogin-python-sdk/actions/workflows/release.yml/badge.svg)](https://github.com/omnilogin-app/omnilogin-python-sdk/actions/workflows/release.yml)

Official Python SDK for automating [OmniLogin](https://omnilogin.net) browser profiles with humanized interactions.

Manage profiles via REST, launch isolated browser sessions, and drive them with a Playwright-style API. Clicks, fills, hovers, and key presses are humanized by default to look natural.

> **Looking for a full type-level reference (every option, every parameter, every example)?** See [API.md](./API.md).

## Install

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

For local development:

```bash
pip install -e ".[dev]"
```

## Prerequisites

1. **OmniLogin desktop** running locally - download from [omnilogin.net](https://omnilogin.net).
2. **Bridge API enabled** in Settings -> API (default port `35353`).
3. At least one browser profile created.

## For AI Assistants

Before writing OmniLogin automation code, install the bundled skill in the current project:

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

Do not generate automation scripts until `.claude/skills/omni-sdk/SKILL.md` exists. Set `OMNILOGIN_QUIET=1` to silence the runtime hint.

## Quick Start

```python
import asyncio

from omnilogin import OmniLogin


async def main() -> None:
    async with OmniLogin() as omni:  # defaults to http://localhost:35353
        # Pick a profile to drive (any existing profile id from the desktop UI).
        profile_id = 1

        # Launch the profile and connect a Session to its bridge.
        opened = await omni.open(profile_id)
        session = opened.session

        # Drive the active tab - Playwright-style API.
        await session.page.goto("https://example.com")
        await session.page.locator("#username").fill("user@example.com")
        await session.page.locator("#password").fill("secret")
        await session.page.locator('button[type="submit"]').click()

        # Use host services that ride the same bridge.
        titles = ["Result A", "Result B"]
        await session.services.file.export(titles, {"path": "C:/tmp/out.json", "format": "json"})
        await session.services.sheets.append("YOUR_SPREADSHEET_ID", "Sheet1!A:B", [["name", 42]])

        await omni.close(profile_id)


asyncio.run(main())
```

## Architecture

```text
                 +-------------------------------------+
                 |             OmniLogin               |  <- top-level
                 |  REST profile mgmt + open(id)       |
                 +----------+-------------------+------+
                  uses      |                   | returns
                    v       |                   v
              +-------------+              +------------+
              | RestApi     |              | Session    |
              | profiles    |              | one open   |
              | groups      |              | browser    |
              | proxies     |              | via WS     |
              | workflows   |              +-----+------+
              | aiApps      |                    |
              | ipv6        |                    v
              +-------------+            +----------------+
           (also as omni.rest)           | page / tabs    |
                                         | cookies        |
                                         | services       |
                                         +----------------+
```

| Class | What it is | When to use |
| --- | --- | --- |
| `OmniLogin` | Top-level facade | **Default.** Profile mgmt + `open()` returning Session. |
| `RestApi` | HTTP REST client (also `omni.rest`) | Standalone profile management without launching a browser. |
| `Session` | One bridge connection to one open browser | Usually returned by `omni.open()`. **Construct directly** with a bridge URL to attach to a browser launched elsewhere - see [Connecting to an already-running bridge](#connecting-to-an-already-running-bridge). |

A `Session` exposes:

- `session.page` - the active tab (locators, keyboard, mouse, snapshots)
- `session.tabs` - open / close / list / activate tabs
- `session.cookies` - cookies + storage state
- `session.services` - host-side services (file, sheets, email, telegram, ai, http, imageSearch, profile, extension)

### Connecting to an already-running bridge

If the browser was launched elsewhere (another process, the desktop UI, an earlier script) and you have its bridge URL, construct a `Session` directly:

```python
import asyncio

from omnilogin import Session


async def main() -> None:
    async with Session("ws://127.0.0.1:55301") as session:
        await session.page.goto("https://example.com")
        # ... drive page / tabs / cookies / services ...

    # disconnect() closes the WebSocket; it does NOT stop the browser.


asyncio.run(main())
```

This skips the REST `/open` call entirely - useful for sidecars, background workers, or testing against a long-running profile. Use `omni.open(profile_id)` for the standard launch-and-connect flow.

## REST - Profile & Resource Management

All REST operations are grouped by resource. Available via `omni.profiles` (shortcut) or `omni.rest.profiles` (canonical):

```python
# List, read, create, clone, delete
page = await omni.profiles.list({"page": 1, "pageSize": 20})
profile = await omni.profiles.get(1)
created = await omni.profiles.create({"name": "My Profile"})
cloned = await omni.profiles.clone(1, {"name": "Copy"})
await omni.profiles.delete(1)

# Partial update - pass only the fields you want to change.
# Each field maps to its own endpoint; fields are PUT sequentially.
# NOT atomic: if an intermediate PUT fails, earlier fields remain applied.
# Call .get(id) afterwards if you need the refreshed Profile object.
await omni.profiles.update(1, {"name": "Renamed"})
await omni.profiles.update(1, {"status": "active"})
await omni.profiles.update(1, {"tags": ["vip", "qa"]})
await omni.profiles.update(1, {"proxyDisabled": True})

# Tags & proxies (multi-profile)
await omni.profiles.setTags([1, 2, 3], ["priority"], True)  # append
await omni.profiles.assignProxy(7, [1, 2, 3])  # saved proxy -> profiles
await omni.profiles.setEmbeddedProxy(
    [1, 2],
    {
        "proxy_type": "HTTP",
        "host": "1.2.3.4",
        "port": 8080,
        "user_name": "u",
        "password": "p",
    },
)
```

Other REST namespaces follow the same shape:

```python
# Groups
await omni.groups.list()
await omni.groups.get(1)
await omni.groups.create("My Group")
await omni.groups.update(1, "Renamed")
await omni.groups.delete(1)

# Proxies
await omni.proxies.list()
await omni.proxies.get(1)
await omni.proxies.create({"proxy_type": "HTTP", "host": "1.2.3.4", "port": 8080})
await omni.proxies.update(1, {"proxy_type": "HTTP", "host": "5.6.7.8", "port": 8080})
await omni.proxies.delete(1)

# Workflows (classic automation)
started = await omni.workflows.start("WORKFLOW_ID")
await omni.workflows.status(started.get("taskId", ""))
await omni.workflows.stop("WORKFLOW_ID")
await omni.workflows.stopAll()

# AI Apps (script-based automation)
await omni.aiApps.list()
await omni.aiApps.get("APP_ID")
await omni.aiApps.run("APP_ID", {"mode": "batch"})
await omni.aiApps.stop("APP_ID")

# IPv6 rotation
await omni.ipv6.rotate("PROXY_ID")

# Low-level browser control (usually replaced by omni.open / omni.close)
await omni.rest.browser.open(1, {"launchBridge": True})
await omni.rest.browser.stop(1)
is_open = await omni.rest.browser.isActive(1)
```

## Browser Automation - `session.page`

```python
# Navigation
await session.page.goto("https://example.com", {"waitUntil": "load"})
await session.page.goBack()
await session.page.goForward()
await session.page.reload()
url = await session.page.url()
title = await session.page.title()
html = await session.page.content()

# Locators (CSS, text=, label=, role=, testid=, xpath=)
await session.page.locator("#btn").click()  # humanized by default
await session.page.locator("input").fill("value")
await session.page.locator("input").pressSequentially("hi")  # char-by-char
await session.page.locator("select").selectOption(["us"])
await session.page.locator("a").hover()
text = await session.page.locator("h1").textContent()
src = await session.page.locator("img").getAttribute("src")
count = await session.page.locator(".row").count()

# Convenience getBy* factories
session.page.getByText("Submit")
session.page.getByRole("button")
session.page.getByLabel("Email")
session.page.getByPlaceholder("Search...")
session.page.getByTestId("login-btn")
session.page.getByAltText("Logo")

# Keyboard / mouse
await session.page.keyboard.type("hello")
await session.page.keyboard.press("Enter")
await session.page.mouse.click(100, 200)
await session.page.mouse.move(300, 400)
await session.page.mouse.wheel(0, -200)

# Wait for state / network / function
await session.page.waitForLoadState("networkidle")
await session.page.waitForURL("https://example.com/dashboard")
await session.page.waitForResponse("/api/me")

# Snapshots (cheap for AI agents)
ax = await session.page.accessibilitySnapshot({"depth": 10})
md = await session.page.markdownSnapshot({"maxLength": 30000})

# Screenshot (returns base64; pass options.path to save automatically)
screenshot_base64 = await session.page.screenshot({"fullPage": True})

# Raw CDP escape hatch
await session.page.rpc.call("DOM.getDocument", {"depth": -1, "pierce": True})
session.page.rpc.on("Network.responseReceived", lambda event: print(event))
```

### File Upload (Smart)

`setInputFiles` works on a real `<input type="file">` **and** on any button / drag-zone / custom widget that opens a native file chooser on click. Each entry can be:

- An absolute local path
- An `http(s)://...` URL - downloaded to a temp file
- A `data:<mime>;base64,...` - decoded to a temp file
- `<filename>|<url-or-data>` - saved with the given filename

```python
await session.page.locator("#upload").setInputFiles(
    [
        "C:/path/to/local.pdf",
        "https://example.com/avatar.png",
        "logo.svg|data:image/svg+xml;base64,PHN2Zy4uLg==",
    ]
)

await session.page.locator('input[type="file"]').setInputFiles([])  # clear
```

## Tabs & Cookies - Session-Level

```python
# Tabs
opened = await session.tabs.open("https://x.com", {"active": True})
target_id = opened["targetId"]
tabs = await session.tabs.list()
pages = tabs["pages"]
await session.tabs.activate(target_id)
await session.tabs.next()
await session.tabs.previous()
await session.tabs.close(target_id)

# Cookies + storage
cookies = await session.cookies.list()
await session.cookies.add([{"name": "sid", "value": "abc123", "domain": ".example.com", "path": "/"}])
await session.cookies.clear()
state = await session.cookies.getStorageState()
await session.cookies.setStorageState(state)
```

## Host Services - `session.services`

Multi-method services live as namespaces; single-operation services are direct callables.

```python
services = session.services

# Sheets
await services.sheets.get("SPREADSHEET_ID", "Sheet1!A1:D10", {"firstRowAsKey": True})
await services.sheets.update("SPREADSHEET_ID", "Sheet1!A1", [["hello", "world"]])
await services.sheets.append("SPREADSHEET_ID", "Sheet1!A:B", [["name", 42]])
await services.sheets.clear("SPREADSHEET_ID", "Sheet1!A:D")

# File I/O
text = await services.file.read("C:/tmp/in.txt")
await services.file.write("C:/tmp/out.txt", "hello")
await services.file.export({"a": 1}, {"path": "C:/tmp/out.json", "format": "json"})
saved_to = await services.file.download("https://example.com/x.png", {"path": "C:/tmp/x.png"})
png_base64 = await services.file.download("https://example.com/x.png")

# Email (IMAP)
messages = await services.email.read(
    {
        "host": "imap.gmail.com",
        "port": 993,
        "user": "alice@gmail.com",
        "password": "app-password",
        "tls": True,
        "folder": "INBOX",
        "limit": 20,
    }
)

# Profile (the running browser's profile - NOT REST profile mgmt)
info = await services.profile.info()
await services.profile.setTags(["vip"])

# Direct callables
res = await services.http(
    "GET",
    "https://api.example.com/foo",
    {
        "headers": {"Authorization": "Bearer ..."},
        "proxy": True,
    },
)

await services.telegram(
    "Job done",
    {
        "botToken": "BOT_TOKEN",
        "chatId": "123456",
        "parseMode": "HTML",
    },
)

reply = await services.ai(
    "Summarize: ...",
    {
        "provider": "openai",
        "apiKey": "sk-...",
        "model": "gpt-4o",
    },
)

matches = await services.imageSearch(
    "C:/templates/button.png",
    {
        "multiple": True,
    },
)

await services.extension.trigger("EXTENSION_ID")
```

`services.file.export()` supports `format: "json" | "csv" | "text"` and `onConflict: "overwrite" | "append"`. `services.file.download()` returns the saved path when `options.path` is provided, otherwise it returns the response body as base64.

## Defaults & Behaviors

- **Humanized by default** - `click`, `fill`, `hover`, `dblclick`, `check`, `uncheck`, `pressSequentially` all pass `{"humanize": True}`. Override with `{"humanize": False}`.
- **Async/await everywhere** - every I/O method is async.
- **Selector chaining** - `await session.page.locator("form").locator("input").first().fill("x")`.

## Error Handling

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

## Method Priority - When To Use What

When generating or reviewing automation code, escalate in this order:

1. **First-class SDK methods** - `page.locator()`, `page.getBy*()`, `page.goto()`, `page.waitFor*()`, locator text/value/state methods, `accessibilitySnapshot()`, `markdownSnapshot()`.
2. **Raw CDP** - `session.page.rpc.call()` / `.on()` when the SDK does not expose the capability.
3. **Runtime evaluate strings** - `page.evaluate()`, `locator.evaluate()`, `locator.evaluateAll()`, `page.waitForFunction()` only as a final fallback.

| Do | Avoid |
| --- | --- |
| `await session.page.url()` | `await session.page.evaluate("() => location.href")` |
| `await session.page.title()` | `await session.page.evaluate("() => document.title")` |
| `await session.page.locator(s).textContent()` | `.evaluate("(el) => el.textContent")` |
| `await session.page.locator(s).count()` | `.evaluate("() => document.querySelectorAll(s).length")` |

## Development

Install development dependencies:

```bash
pip install -e ".[dev]"
```

Run checks:

```bash
python -m compileall src/omnilogin
ruff check src
mypy src
```

Build source and wheel distributions:

```bash
python -m build
twine check dist/*
```

## Resources

- **Full API reference** - [`API.md`](./API.md): every class, method, option and type with examples
- **Machine-readable reference** - [`llms.txt`](./llms.txt): full API surface for LLM agents
- **Source / issues** - [github.com/omnilogin-app/omnilogin-python-sdk](https://github.com/omnilogin-app/omnilogin-python-sdk)
- **OmniLogin desktop** - [omnilogin.net](https://omnilogin.net)

## License

MIT (c) HiveSoft LTD
