oculr 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
oculr/__init__.py ADDED
@@ -0,0 +1,145 @@
1
+ """oculr — a thin client for the Oculr local Automation API.
2
+
3
+ Zero runtime dependencies (stdlib ``urllib``). Point it at your local agent (the
4
+ desktop app shows the port and token under Settings) and you get a ready CDP
5
+ endpoint back, which any driver can attach to.
6
+
7
+ from oculr import OculrClient
8
+ from playwright.sync_api import sync_playwright
9
+
10
+ oculr = OculrClient.from_agent_file() # auto-discovers base_url + token
11
+ res = oculr.launch("de-store-01", stealth="balanced", headless=False)
12
+
13
+ with sync_playwright() as p:
14
+ browser, _ = oculr.connect_playwright(p, "de-store-01", stealth="max")
15
+ page = browser.contexts[0].pages[0]
16
+ page.goto("https://example.com")
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import urllib.error
24
+ import urllib.request
25
+ from typing import Any, Optional
26
+
27
+ __all__ = ["OculrClient", "OculrError"]
28
+ __version__ = "0.1.0"
29
+
30
+
31
+ class OculrError(Exception):
32
+ """Raised on a non-2xx response from the Oculr API."""
33
+
34
+ def __init__(self, status: int, body: Any, message: str) -> None:
35
+ super().__init__(message)
36
+ self.status = status
37
+ self.body = body
38
+
39
+
40
+ class OculrClient:
41
+ """Client for the Oculr local Automation API.
42
+
43
+ :param base_url: Base URL of the local agent, e.g. ``http://127.0.0.1:8765``.
44
+ :param token: Bearer token from the desktop app Settings (omit if auth is off).
45
+ """
46
+
47
+ def __init__(self, base_url: str, token: Optional[str] = None, timeout: float = 30.0) -> None:
48
+ if not base_url:
49
+ raise ValueError("OculrClient: base_url is required")
50
+ self.base_url = base_url.rstrip("/")
51
+ self.token = token
52
+ self.timeout = timeout
53
+
54
+ @classmethod
55
+ def from_agent_file(cls, path: Optional[str] = None, timeout: float = 30.0) -> "OculrClient":
56
+ """Build a client from the running agent's 0600 runtime descriptor.
57
+
58
+ The desktop app (and ``oculr-agent serve``) writes an ``agent.json`` with
59
+ the agent's ``base_url`` and — when token publishing is on — its auth
60
+ ``token``, so you don't have to copy them from Settings. Searches
61
+ ``$OCULR_RUNTIME_FILE``, then ``$OCULR_DATA_DIR/agent.json``, then
62
+ ``~/.oculr/data/agent.json``. Raises :class:`OculrError` if none is found.
63
+ """
64
+ candidates = []
65
+ if path:
66
+ candidates.append(path)
67
+ if os.environ.get("OCULR_RUNTIME_FILE"):
68
+ candidates.append(os.environ["OCULR_RUNTIME_FILE"])
69
+ if os.environ.get("OCULR_DATA_DIR"):
70
+ candidates.append(os.path.join(os.environ["OCULR_DATA_DIR"], "agent.json"))
71
+ candidates.append(os.path.expanduser("~/.oculr/data/agent.json"))
72
+ for candidate in candidates:
73
+ try:
74
+ with open(candidate, encoding="utf-8") as fh:
75
+ desc = json.load(fh)
76
+ except (OSError, ValueError):
77
+ continue
78
+ base = desc.get("base_url")
79
+ if base:
80
+ return cls(base, token=desc.get("token"), timeout=timeout)
81
+ raise OculrError(0, None, "no Oculr agent descriptor found (is the app running?)")
82
+
83
+ def _request(self, method: str, path: str, body: Any = None) -> Any:
84
+ data = json.dumps(body).encode() if body is not None else None
85
+ req = urllib.request.Request(self.base_url + path, data=data, method=method)
86
+ req.add_header("Content-Type", "application/json")
87
+ if self.token:
88
+ req.add_header("Authorization", f"Bearer {self.token}")
89
+ try:
90
+ with urllib.request.urlopen(req, timeout=self.timeout) as resp:
91
+ text = resp.read().decode()
92
+ return json.loads(text) if text else None
93
+ except urllib.error.HTTPError as e: # pragma: no cover - exercised via mock
94
+ text = e.read().decode()
95
+ parsed = json.loads(text) if text else None
96
+ raise OculrError(e.code, parsed, f"Oculr {method} {path} failed: {e.code}") from None
97
+
98
+ # ── Profiles ────────────────────────────────────────────────────────────
99
+ def list_profiles(self) -> Any:
100
+ return self._request("GET", "/api/profiles")
101
+
102
+ def create_profile(self, **body: Any) -> Any:
103
+ return self._request("POST", "/api/profiles", body)
104
+
105
+ def get_profile(self, profile_id: str) -> Any:
106
+ return self._request("GET", f"/api/profiles/{profile_id}")
107
+
108
+ def update_profile(self, profile_id: str, **body: Any) -> Any:
109
+ return self._request("PUT", f"/api/profiles/{profile_id}", body)
110
+
111
+ def delete_profile(self, profile_id: str) -> Any:
112
+ return self._request("DELETE", f"/api/profiles/{profile_id}")
113
+
114
+ def generate_profile(self, **body: Any) -> Any:
115
+ """Generate a coherent random fingerprint."""
116
+ return self._request("POST", "/api/profiles/generate", body or {})
117
+
118
+ # ── Lifecycle ───────────────────────────────────────────────────────────
119
+ def launch(self, profile_id: str, **options: Any) -> Any:
120
+ """Launch a profile. With no options the stored profile defaults are used.
121
+
122
+ Options (all per-launch, never written back): ``stealth`` ("off" |
123
+ "balanced" | "max"), ``headless``, ``proxy``, ``args``, ``window``,
124
+ ``automation``, ``ephemeral``.
125
+ """
126
+ return self._request("POST", f"/api/profiles/{profile_id}/launch", options or None)
127
+
128
+ def stop(self, profile_id: str) -> Any:
129
+ return self._request("POST", f"/api/profiles/{profile_id}/stop")
130
+
131
+ def status(self) -> Any:
132
+ return self._request("GET", "/api/status")
133
+
134
+ # ── High-level ──────────────────────────────────────────────────────────
135
+ def connect_playwright(self, playwright: Any, profile_id: str, **options: Any):
136
+ """Launch a profile and connect a Playwright browser over CDP.
137
+
138
+ Pass the value from ``sync_playwright()``. Returns ``(browser, response)``.
139
+ """
140
+ res = self.launch(profile_id, **options)
141
+ ws = (res or {}).get("ws_endpoint")
142
+ if not ws:
143
+ raise OculrError(0, res, "launch did not return a ws_endpoint")
144
+ browser = playwright.chromium.connect_over_cdp(ws)
145
+ return browser, res
@@ -0,0 +1,68 @@
1
+ Metadata-Version: 2.4
2
+ Name: oculr
3
+ Version: 0.1.0
4
+ Summary: Client for the Oculr local Automation API: launch real browser profiles, pick a per-launch stealth level, and connect over CDP.
5
+ Project-URL: Homepage, https://oculr.ai
6
+ Project-URL: Documentation, https://oculr.ai/developers
7
+ Project-URL: Repository, https://github.com/oculr-ai/oculr-sdk
8
+ License: MIT
9
+ Keywords: ai-agent,ai-agents,anti-detect,antidetect,antidetect-browser,automation,browser,browser-automation,cdp,fingerprint,mcp,model-context-protocol,oculr,playwright,puppeteer,stealth
10
+ Requires-Python: >=3.9
11
+ Provides-Extra: playwright
12
+ Requires-Dist: playwright; extra == 'playwright'
13
+ Description-Content-Type: text/markdown
14
+
15
+ # oculr (Python)
16
+
17
+ Thin Python client for the **Oculr local Automation API**. Launch real browser
18
+ profiles on your own machine, choose a per-launch stealth level, and connect over
19
+ CDP with Playwright or Selenium.
20
+
21
+ Zero runtime dependencies (stdlib only). Playwright is optional, for the
22
+ `connect_playwright()` helper.
23
+
24
+ ## Install
25
+
26
+ ```bash
27
+ pip install oculr
28
+ # optional, for connect_playwright():
29
+ pip install playwright
30
+ ```
31
+
32
+ ## Usage
33
+
34
+ ```python
35
+ import os
36
+ from oculr import OculrClient
37
+ from playwright.sync_api import sync_playwright
38
+
39
+ # The desktop app shows the port and token under Settings.
40
+ oculr = OculrClient("http://127.0.0.1:8378", token=os.environ["OCULR_TOKEN"])
41
+
42
+ # Low-level: launch and get a ready CDP endpoint + driver recipes.
43
+ res = oculr.launch("de-store-01", stealth="balanced", headless=False)
44
+ print(res["ws_endpoint"])
45
+
46
+ # High-level: launch and connect a Playwright browser in one call.
47
+ with sync_playwright() as p:
48
+ browser, _ = oculr.connect_playwright(p, "de-store-01", stealth="max")
49
+ page = browser.contexts[0].pages[0]
50
+ page.goto("https://example.com")
51
+ oculr.stop("de-store-01")
52
+ ```
53
+
54
+ ### Stealth dial
55
+
56
+ `stealth` is chosen per launch: `"off"` (clean Chromium, no fingerprint
57
+ injection), `"balanced"` (native fingerprints, the default) or `"max"` (strongest
58
+ noise). Overrides apply to that launch only and are never written back to the
59
+ profile.
60
+
61
+ ## API
62
+
63
+ `list_profiles()`, `create_profile(**body)`, `get_profile(id)`,
64
+ `update_profile(id, **body)`, `delete_profile(id)`, `generate_profile(**body)`,
65
+ `launch(id, **options)`, `stop(id)`, `status()`,
66
+ `connect_playwright(playwright, id, **options)`.
67
+
68
+ Errors raise `OculrError` with `.status` and `.body`.
@@ -0,0 +1,4 @@
1
+ oculr/__init__.py,sha256=sggVeMW7icBNKWc1TDPRr8iwKvsXxAzsR3dCOxsq5Xg,6531
2
+ oculr-0.1.0.dist-info/METADATA,sha256=9uvso9X_nSPb5AuDyLVmL6u8DgnGn-b93EoNgZBxUP0,2366
3
+ oculr-0.1.0.dist-info/WHEEL,sha256=lCkmxWfQsSc9CfIClYeavTdQeEX2toPqufh9gI35EQA,87
4
+ oculr-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.31.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any