helixwright 0.1.0__tar.gz
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.
- helixwright-0.1.0/PKG-INFO +240 -0
- helixwright-0.1.0/README.md +221 -0
- helixwright-0.1.0/helixwright/__init__.py +58 -0
- helixwright-0.1.0/helixwright/_impl/__init__.py +1 -0
- helixwright-0.1.0/helixwright/_impl/_assertions.py +184 -0
- helixwright-0.1.0/helixwright/_impl/_consent.py +80 -0
- helixwright-0.1.0/helixwright/_impl/_errors.py +44 -0
- helixwright-0.1.0/helixwright/_impl/_forensics.py +202 -0
- helixwright-0.1.0/helixwright/_impl/_humanize.py +250 -0
- helixwright-0.1.0/helixwright/_impl/_input.py +238 -0
- helixwright-0.1.0/helixwright/_impl/_locator.py +1322 -0
- helixwright-0.1.0/helixwright/_impl/_network.py +265 -0
- helixwright-0.1.0/helixwright/_impl/_operator.py +746 -0
- helixwright-0.1.0/helixwright/_impl/_page.py +2585 -0
- helixwright-0.1.0/helixwright/_impl/_search.py +2 -0
- helixwright-0.1.0/helixwright/_impl/_selectors.py +322 -0
- helixwright-0.1.0/helixwright/_impl/_settings.py +51 -0
- helixwright-0.1.0/helixwright/_impl/_transport.py +127 -0
- helixwright-0.1.0/helixwright/_impl/_waits.py +204 -0
- helixwright-0.1.0/helixwright/client.py +504 -0
- helixwright-0.1.0/helixwright/errors.py +50 -0
- helixwright-0.1.0/helixwright/launcher.py +267 -0
- helixwright-0.1.0/helixwright/models.py +117 -0
- helixwright-0.1.0/helixwright/py.typed +1 -0
- helixwright-0.1.0/helixwright/solver.py +107 -0
- helixwright-0.1.0/helixwright.egg-info/PKG-INFO +240 -0
- helixwright-0.1.0/helixwright.egg-info/SOURCES.txt +33 -0
- helixwright-0.1.0/helixwright.egg-info/dependency_links.txt +1 -0
- helixwright-0.1.0/helixwright.egg-info/top_level.txt +1 -0
- helixwright-0.1.0/pyproject.toml +36 -0
- helixwright-0.1.0/setup.cfg +4 -0
- helixwright-0.1.0/tests/test_helixwright_local_api.py +342 -0
- helixwright-0.1.0/tests/test_public_api.py +144 -0
- helixwright-0.1.0/tests/test_rpc_contract.py +80 -0
- helixwright-0.1.0/tests/test_selector_grammar.py +60 -0
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: helixwright
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python automation SDK for Helix Browser Local API and the private Helix RPC page-control plane.
|
|
5
|
+
Author: Helix Browser
|
|
6
|
+
License-Expression: LicenseRef-Proprietary
|
|
7
|
+
Project-URL: Homepage, https://pypi.org/project/helixwright/
|
|
8
|
+
Project-URL: Documentation, https://pypi.org/project/helixwright/
|
|
9
|
+
Keywords: automation,browser,fingerprint,chromium,local-api,helix
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
12
|
+
Classifier: Typing :: Typed
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
15
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: Browsers
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Requires-Python: >=3.9
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# Helixwright
|
|
21
|
+
|
|
22
|
+
Helixwright is the Python automation SDK for Helix Browser.
|
|
23
|
+
|
|
24
|
+
It does not create fingerprints locally, manage browser profile folders, or
|
|
25
|
+
start `chrome.exe` directly. Profile creation, fingerprint generation, proxy
|
|
26
|
+
configuration, browser launch, and profile sync are delegated to Helix Browser
|
|
27
|
+
Desktop through its Local API. Page actions then use the private Helix RPC
|
|
28
|
+
control plane built into the Helix Chromium fork, not CDP, DevTools, WebDriver,
|
|
29
|
+
Playwright, or Selenium.
|
|
30
|
+
|
|
31
|
+
## Install
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install helixwright
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Requirements:
|
|
38
|
+
|
|
39
|
+
1. Helix Browser Desktop is installed and running.
|
|
40
|
+
2. The desktop app is logged in.
|
|
41
|
+
3. The desktop Local API is available, usually at `http://127.0.0.1:59777`.
|
|
42
|
+
4. The desktop build supports `/api/v1/automation/sessions`.
|
|
43
|
+
|
|
44
|
+
Helixwright discovers the Local API from:
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
%APPDATA%\Helix Browser\local_api.json
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
You can override it explicitly:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
$env:HELIX_LOCAL_API = "http://127.0.0.1:59777"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
Reuse an existing Helix profile:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import helixwright as hw
|
|
62
|
+
|
|
63
|
+
with hw.launch(profile_id="profile_123", url="https://example.com") as page:
|
|
64
|
+
page.ele("#email").input("user@example.com")
|
|
65
|
+
page.ele("#submit").click()
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Create or reuse a profile by `account_id`:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
import helixwright as hw
|
|
72
|
+
|
|
73
|
+
with hw.launch(
|
|
74
|
+
"https://example.com",
|
|
75
|
+
account_id="shop-001",
|
|
76
|
+
name="Shop 001",
|
|
77
|
+
proxy="http://user:pass@127.0.0.1:7890",
|
|
78
|
+
fingerprint=hw.Fingerprint(locale="en-US", region="US", group="octo"),
|
|
79
|
+
update_existing=True,
|
|
80
|
+
) as page:
|
|
81
|
+
page.wait_for("body", timeout=10_000)
|
|
82
|
+
print(page.title())
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Use a reusable launch config:
|
|
86
|
+
|
|
87
|
+
```python
|
|
88
|
+
import helixwright as hw
|
|
89
|
+
|
|
90
|
+
cfg = hw.LaunchConfig(
|
|
91
|
+
account_id="shop-002",
|
|
92
|
+
name="Shop 002",
|
|
93
|
+
url="https://example.com",
|
|
94
|
+
start_pages=["https://example.com", "https://example.com/login"],
|
|
95
|
+
proxy="http://127.0.0.1:7890",
|
|
96
|
+
fingerprint=hw.Fingerprint(locale="en-US", region="US"),
|
|
97
|
+
update_existing=True,
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
with hw.launch(config=cfg) as page:
|
|
101
|
+
page.ele("text=Login").click()
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Client API
|
|
105
|
+
|
|
106
|
+
Use `Client` when you want to manage profiles, fingerprints, proxies, cores, or
|
|
107
|
+
automation sessions explicitly:
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
import helixwright as hw
|
|
111
|
+
|
|
112
|
+
client = hw.Client()
|
|
113
|
+
print(client.health())
|
|
114
|
+
print(client.session())
|
|
115
|
+
|
|
116
|
+
profile = client.profiles.ensure(
|
|
117
|
+
account_id="shop-003",
|
|
118
|
+
name="Shop 003",
|
|
119
|
+
proxy=hw.Proxy("http", "127.0.0.1", 7890),
|
|
120
|
+
fingerprint=hw.Fingerprint(locale="en-US", region="US"),
|
|
121
|
+
update_existing=True,
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
with profile.launch(url="https://example.com") as page:
|
|
125
|
+
print(page.url)
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Common managers:
|
|
129
|
+
|
|
130
|
+
```python
|
|
131
|
+
client.profiles.list(page=1, size=20)
|
|
132
|
+
client.profiles.get("profile_123")
|
|
133
|
+
client.profiles.create(name="A", account_id="acct_A")
|
|
134
|
+
client.profiles.ensure(account_id="acct_A")
|
|
135
|
+
client.profiles.clone("profile_123", count=2)
|
|
136
|
+
client.profiles.sync_up("profile_123")
|
|
137
|
+
client.profiles.sync_down("profile_123")
|
|
138
|
+
|
|
139
|
+
client.fingerprint.options()
|
|
140
|
+
client.fingerprint.draw(account_id="acct_A", group="octo")
|
|
141
|
+
client.fingerprint.serialize({"ua": "..."}, seed=123)
|
|
142
|
+
client.fingerprint.redraw({"ua": "..."}, "ua")
|
|
143
|
+
|
|
144
|
+
client.proxy.check("http://127.0.0.1:7890")
|
|
145
|
+
client.cores.list()
|
|
146
|
+
client.cores.ensure("chromium", "150")
|
|
147
|
+
client.browser.active()
|
|
148
|
+
client.browser.start("profile_123")
|
|
149
|
+
client.browser.stop("profile_123")
|
|
150
|
+
client.browser.stop_all()
|
|
151
|
+
client.automation.list()
|
|
152
|
+
client.automation.start("profile_123", url="https://example.com")
|
|
153
|
+
client.automation.stop("auto_123")
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Launch Rules
|
|
157
|
+
|
|
158
|
+
`hw.launch()` requires either `profile_id` or `account_id`/`account`.
|
|
159
|
+
|
|
160
|
+
Important behavior:
|
|
161
|
+
|
|
162
|
+
- `profile_id` means reuse that exact Helix profile.
|
|
163
|
+
- `account_id` means find an existing profile by account ID, or create one
|
|
164
|
+
when `create=True`.
|
|
165
|
+
- `fingerprint=hw.Fingerprint(...)` only sends creation hints to Helix Local
|
|
166
|
+
API. The SDK does not generate fingerprints itself.
|
|
167
|
+
- `proxy` can be a string, dict, or `hw.Proxy`.
|
|
168
|
+
- `start_pages` is passed to the Local API automation session.
|
|
169
|
+
- `config=hw.LaunchConfig(...)` should not be mixed with explicit
|
|
170
|
+
`profile_id`, `account_id`, `name`, `proxy`, or `fingerprint` arguments.
|
|
171
|
+
|
|
172
|
+
Unsupported legacy direct-launch arguments are rejected, including `chrome`,
|
|
173
|
+
`user_data_dir`, `persona`, `seed`, `geoip`, and `extra_args`.
|
|
174
|
+
|
|
175
|
+
## Errors
|
|
176
|
+
|
|
177
|
+
Common error types:
|
|
178
|
+
|
|
179
|
+
| Error | Meaning |
|
|
180
|
+
| --- | --- |
|
|
181
|
+
| `LocalApiUnavailable` | Helix Desktop is not running, the Local API address is wrong, or the port is unreachable. |
|
|
182
|
+
| `DesktopNotLoggedIn` | Desktop is reachable but not logged in. |
|
|
183
|
+
| `LocalApiError` | Local API returned a business error. |
|
|
184
|
+
| `ProfileNotFound` | The requested profile does not exist. |
|
|
185
|
+
| `AutomationEndpointUnavailable` | The desktop build did not return a usable automation RPC endpoint. |
|
|
186
|
+
| `AutomationSessionNotFound` | The requested automation session does not exist. |
|
|
187
|
+
| `TransportError` | The private page RPC connection failed, usually because the browser exited. |
|
|
188
|
+
| `WaitTimeoutError` | A page wait condition timed out. |
|
|
189
|
+
|
|
190
|
+
## Examples
|
|
191
|
+
|
|
192
|
+
From the source repository:
|
|
193
|
+
|
|
194
|
+
```powershell
|
|
195
|
+
cd "E:\Helix Browser\repos\helixwright"
|
|
196
|
+
python examples\99_offline_mock_demo.py
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The offline demo does not need Helix Browser. It starts a mock Local API and
|
|
200
|
+
mock RPC endpoint, then exercises the public SDK surface.
|
|
201
|
+
|
|
202
|
+
Real desktop examples require Helix Browser Desktop to be running and logged in:
|
|
203
|
+
|
|
204
|
+
```powershell
|
|
205
|
+
python examples\00_helixwright_quickstart.py
|
|
206
|
+
python examples\01_client_managers.py
|
|
207
|
+
python examples\02_launch_config_and_actions.py
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Useful environment variables:
|
|
211
|
+
|
|
212
|
+
```powershell
|
|
213
|
+
$env:HELIX_LOCAL_API = "http://127.0.0.1:59777"
|
|
214
|
+
$env:HELIX_EXAMPLE_ACCOUNT = "demo-account"
|
|
215
|
+
$env:HELIX_EXAMPLE_URL = "https://example.com"
|
|
216
|
+
$env:HELIX_EXAMPLE_PROXY = "http://127.0.0.1:7890"
|
|
217
|
+
$env:HELIX_EXAMPLE_TIMEOUT_MS = "30000"
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Development
|
|
221
|
+
|
|
222
|
+
```powershell
|
|
223
|
+
cd "E:\Helix Browser\repos\helixwright"
|
|
224
|
+
pip install -e .
|
|
225
|
+
python tests\ci.py
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Build and validate a distribution:
|
|
229
|
+
|
|
230
|
+
```powershell
|
|
231
|
+
python -m build
|
|
232
|
+
python -m twine check dist/*
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
## Boundary
|
|
236
|
+
|
|
237
|
+
Helixwright is only the automation SDK. The authoritative source for profiles,
|
|
238
|
+
fingerprints, proxies, browser core downloads, cloud sync, and launch lifecycle
|
|
239
|
+
is Helix Browser Desktop plus its Local API.
|
|
240
|
+
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
# Helixwright
|
|
2
|
+
|
|
3
|
+
Helixwright is the Python automation SDK for Helix Browser.
|
|
4
|
+
|
|
5
|
+
It does not create fingerprints locally, manage browser profile folders, or
|
|
6
|
+
start `chrome.exe` directly. Profile creation, fingerprint generation, proxy
|
|
7
|
+
configuration, browser launch, and profile sync are delegated to Helix Browser
|
|
8
|
+
Desktop through its Local API. Page actions then use the private Helix RPC
|
|
9
|
+
control plane built into the Helix Chromium fork, not CDP, DevTools, WebDriver,
|
|
10
|
+
Playwright, or Selenium.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install helixwright
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Requirements:
|
|
19
|
+
|
|
20
|
+
1. Helix Browser Desktop is installed and running.
|
|
21
|
+
2. The desktop app is logged in.
|
|
22
|
+
3. The desktop Local API is available, usually at `http://127.0.0.1:59777`.
|
|
23
|
+
4. The desktop build supports `/api/v1/automation/sessions`.
|
|
24
|
+
|
|
25
|
+
Helixwright discovers the Local API from:
|
|
26
|
+
|
|
27
|
+
```text
|
|
28
|
+
%APPDATA%\Helix Browser\local_api.json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
You can override it explicitly:
|
|
32
|
+
|
|
33
|
+
```powershell
|
|
34
|
+
$env:HELIX_LOCAL_API = "http://127.0.0.1:59777"
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Quick Start
|
|
38
|
+
|
|
39
|
+
Reuse an existing Helix profile:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
import helixwright as hw
|
|
43
|
+
|
|
44
|
+
with hw.launch(profile_id="profile_123", url="https://example.com") as page:
|
|
45
|
+
page.ele("#email").input("user@example.com")
|
|
46
|
+
page.ele("#submit").click()
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Create or reuse a profile by `account_id`:
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import helixwright as hw
|
|
53
|
+
|
|
54
|
+
with hw.launch(
|
|
55
|
+
"https://example.com",
|
|
56
|
+
account_id="shop-001",
|
|
57
|
+
name="Shop 001",
|
|
58
|
+
proxy="http://user:pass@127.0.0.1:7890",
|
|
59
|
+
fingerprint=hw.Fingerprint(locale="en-US", region="US", group="octo"),
|
|
60
|
+
update_existing=True,
|
|
61
|
+
) as page:
|
|
62
|
+
page.wait_for("body", timeout=10_000)
|
|
63
|
+
print(page.title())
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use a reusable launch config:
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
import helixwright as hw
|
|
70
|
+
|
|
71
|
+
cfg = hw.LaunchConfig(
|
|
72
|
+
account_id="shop-002",
|
|
73
|
+
name="Shop 002",
|
|
74
|
+
url="https://example.com",
|
|
75
|
+
start_pages=["https://example.com", "https://example.com/login"],
|
|
76
|
+
proxy="http://127.0.0.1:7890",
|
|
77
|
+
fingerprint=hw.Fingerprint(locale="en-US", region="US"),
|
|
78
|
+
update_existing=True,
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
with hw.launch(config=cfg) as page:
|
|
82
|
+
page.ele("text=Login").click()
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Client API
|
|
86
|
+
|
|
87
|
+
Use `Client` when you want to manage profiles, fingerprints, proxies, cores, or
|
|
88
|
+
automation sessions explicitly:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
import helixwright as hw
|
|
92
|
+
|
|
93
|
+
client = hw.Client()
|
|
94
|
+
print(client.health())
|
|
95
|
+
print(client.session())
|
|
96
|
+
|
|
97
|
+
profile = client.profiles.ensure(
|
|
98
|
+
account_id="shop-003",
|
|
99
|
+
name="Shop 003",
|
|
100
|
+
proxy=hw.Proxy("http", "127.0.0.1", 7890),
|
|
101
|
+
fingerprint=hw.Fingerprint(locale="en-US", region="US"),
|
|
102
|
+
update_existing=True,
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
with profile.launch(url="https://example.com") as page:
|
|
106
|
+
print(page.url)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
Common managers:
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
client.profiles.list(page=1, size=20)
|
|
113
|
+
client.profiles.get("profile_123")
|
|
114
|
+
client.profiles.create(name="A", account_id="acct_A")
|
|
115
|
+
client.profiles.ensure(account_id="acct_A")
|
|
116
|
+
client.profiles.clone("profile_123", count=2)
|
|
117
|
+
client.profiles.sync_up("profile_123")
|
|
118
|
+
client.profiles.sync_down("profile_123")
|
|
119
|
+
|
|
120
|
+
client.fingerprint.options()
|
|
121
|
+
client.fingerprint.draw(account_id="acct_A", group="octo")
|
|
122
|
+
client.fingerprint.serialize({"ua": "..."}, seed=123)
|
|
123
|
+
client.fingerprint.redraw({"ua": "..."}, "ua")
|
|
124
|
+
|
|
125
|
+
client.proxy.check("http://127.0.0.1:7890")
|
|
126
|
+
client.cores.list()
|
|
127
|
+
client.cores.ensure("chromium", "150")
|
|
128
|
+
client.browser.active()
|
|
129
|
+
client.browser.start("profile_123")
|
|
130
|
+
client.browser.stop("profile_123")
|
|
131
|
+
client.browser.stop_all()
|
|
132
|
+
client.automation.list()
|
|
133
|
+
client.automation.start("profile_123", url="https://example.com")
|
|
134
|
+
client.automation.stop("auto_123")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
## Launch Rules
|
|
138
|
+
|
|
139
|
+
`hw.launch()` requires either `profile_id` or `account_id`/`account`.
|
|
140
|
+
|
|
141
|
+
Important behavior:
|
|
142
|
+
|
|
143
|
+
- `profile_id` means reuse that exact Helix profile.
|
|
144
|
+
- `account_id` means find an existing profile by account ID, or create one
|
|
145
|
+
when `create=True`.
|
|
146
|
+
- `fingerprint=hw.Fingerprint(...)` only sends creation hints to Helix Local
|
|
147
|
+
API. The SDK does not generate fingerprints itself.
|
|
148
|
+
- `proxy` can be a string, dict, or `hw.Proxy`.
|
|
149
|
+
- `start_pages` is passed to the Local API automation session.
|
|
150
|
+
- `config=hw.LaunchConfig(...)` should not be mixed with explicit
|
|
151
|
+
`profile_id`, `account_id`, `name`, `proxy`, or `fingerprint` arguments.
|
|
152
|
+
|
|
153
|
+
Unsupported legacy direct-launch arguments are rejected, including `chrome`,
|
|
154
|
+
`user_data_dir`, `persona`, `seed`, `geoip`, and `extra_args`.
|
|
155
|
+
|
|
156
|
+
## Errors
|
|
157
|
+
|
|
158
|
+
Common error types:
|
|
159
|
+
|
|
160
|
+
| Error | Meaning |
|
|
161
|
+
| --- | --- |
|
|
162
|
+
| `LocalApiUnavailable` | Helix Desktop is not running, the Local API address is wrong, or the port is unreachable. |
|
|
163
|
+
| `DesktopNotLoggedIn` | Desktop is reachable but not logged in. |
|
|
164
|
+
| `LocalApiError` | Local API returned a business error. |
|
|
165
|
+
| `ProfileNotFound` | The requested profile does not exist. |
|
|
166
|
+
| `AutomationEndpointUnavailable` | The desktop build did not return a usable automation RPC endpoint. |
|
|
167
|
+
| `AutomationSessionNotFound` | The requested automation session does not exist. |
|
|
168
|
+
| `TransportError` | The private page RPC connection failed, usually because the browser exited. |
|
|
169
|
+
| `WaitTimeoutError` | A page wait condition timed out. |
|
|
170
|
+
|
|
171
|
+
## Examples
|
|
172
|
+
|
|
173
|
+
From the source repository:
|
|
174
|
+
|
|
175
|
+
```powershell
|
|
176
|
+
cd "E:\Helix Browser\repos\helixwright"
|
|
177
|
+
python examples\99_offline_mock_demo.py
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
The offline demo does not need Helix Browser. It starts a mock Local API and
|
|
181
|
+
mock RPC endpoint, then exercises the public SDK surface.
|
|
182
|
+
|
|
183
|
+
Real desktop examples require Helix Browser Desktop to be running and logged in:
|
|
184
|
+
|
|
185
|
+
```powershell
|
|
186
|
+
python examples\00_helixwright_quickstart.py
|
|
187
|
+
python examples\01_client_managers.py
|
|
188
|
+
python examples\02_launch_config_and_actions.py
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
Useful environment variables:
|
|
192
|
+
|
|
193
|
+
```powershell
|
|
194
|
+
$env:HELIX_LOCAL_API = "http://127.0.0.1:59777"
|
|
195
|
+
$env:HELIX_EXAMPLE_ACCOUNT = "demo-account"
|
|
196
|
+
$env:HELIX_EXAMPLE_URL = "https://example.com"
|
|
197
|
+
$env:HELIX_EXAMPLE_PROXY = "http://127.0.0.1:7890"
|
|
198
|
+
$env:HELIX_EXAMPLE_TIMEOUT_MS = "30000"
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
## Development
|
|
202
|
+
|
|
203
|
+
```powershell
|
|
204
|
+
cd "E:\Helix Browser\repos\helixwright"
|
|
205
|
+
pip install -e .
|
|
206
|
+
python tests\ci.py
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
Build and validate a distribution:
|
|
210
|
+
|
|
211
|
+
```powershell
|
|
212
|
+
python -m build
|
|
213
|
+
python -m twine check dist/*
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## Boundary
|
|
217
|
+
|
|
218
|
+
Helixwright is only the automation SDK. The authoritative source for profiles,
|
|
219
|
+
fingerprints, proxies, browser core downloads, cloud sync, and launch lifecycle
|
|
220
|
+
is Helix Browser Desktop plus its Local API.
|
|
221
|
+
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""Helixwright automation SDK.
|
|
2
|
+
|
|
3
|
+
The SDK talks to Helix Browser through the desktop Local API. It does not spawn
|
|
4
|
+
Chrome, generate fingerprints, or manage browser profile directories by itself.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from .client import Client, Profile, discover_base_url
|
|
8
|
+
from .errors import (
|
|
9
|
+
AutomationEndpointUnavailable,
|
|
10
|
+
AutomationSessionNotFound,
|
|
11
|
+
DesktopNotLoggedIn,
|
|
12
|
+
ElementNotFoundError,
|
|
13
|
+
HelixwrightError,
|
|
14
|
+
LocalApiError,
|
|
15
|
+
LocalApiUnavailable,
|
|
16
|
+
LoginRejected,
|
|
17
|
+
ProfileNotFound,
|
|
18
|
+
ReachabilityError,
|
|
19
|
+
TransportError,
|
|
20
|
+
WaitTimeoutError,
|
|
21
|
+
)
|
|
22
|
+
from ._impl._network import Response, Route
|
|
23
|
+
from ._impl._operator import El, NoneEl, Op
|
|
24
|
+
from .launcher import HelixBrowser, attach, launch
|
|
25
|
+
from .models import AutomationRpc, AutomationSession, Fingerprint, LaunchConfig, Proxy
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"launch",
|
|
29
|
+
"attach",
|
|
30
|
+
"Client",
|
|
31
|
+
"Profile",
|
|
32
|
+
"Fingerprint",
|
|
33
|
+
"Proxy",
|
|
34
|
+
"LaunchConfig",
|
|
35
|
+
"AutomationRpc",
|
|
36
|
+
"AutomationSession",
|
|
37
|
+
"HelixBrowser",
|
|
38
|
+
"Op",
|
|
39
|
+
"El",
|
|
40
|
+
"NoneEl",
|
|
41
|
+
"Response",
|
|
42
|
+
"Route",
|
|
43
|
+
"discover_base_url",
|
|
44
|
+
"HelixwrightError",
|
|
45
|
+
"LocalApiError",
|
|
46
|
+
"LocalApiUnavailable",
|
|
47
|
+
"DesktopNotLoggedIn",
|
|
48
|
+
"ProfileNotFound",
|
|
49
|
+
"AutomationSessionNotFound",
|
|
50
|
+
"AutomationEndpointUnavailable",
|
|
51
|
+
"TransportError",
|
|
52
|
+
"ReachabilityError",
|
|
53
|
+
"ElementNotFoundError",
|
|
54
|
+
"WaitTimeoutError",
|
|
55
|
+
"LoginRejected",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Private implementation package (unstable internals). Import the public API from `helixwright`, not from here."""
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
import json
|
|
3
|
+
import time
|
|
4
|
+
from ._errors import HelixwrightError
|
|
5
|
+
|
|
6
|
+
# The accessibility matchers now read via NATIVE RPC helpers on the Locator (_aria_role /
|
|
7
|
+
# _accessible_name / _accessible_description over get_attribute + element_property('tagName')),
|
|
8
|
+
# so NO JS runs. They remain APPROXIMATE (heuristic, not Chromium's computed accessibility tree).
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class _AssertionsBase:
|
|
12
|
+
"""Polling assertion base: shared timeout, negation (not_/is_not), and the poll loop."""
|
|
13
|
+
|
|
14
|
+
def __init__(self, target, timeout_ms=5000, is_not=False):
|
|
15
|
+
self._t = target
|
|
16
|
+
self._to = timeout_ms
|
|
17
|
+
self._not = is_not
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def not_(self):
|
|
21
|
+
return type(self)(self._t, self._to, not self._not)
|
|
22
|
+
|
|
23
|
+
is_not = not_
|
|
24
|
+
|
|
25
|
+
def _poll(self, pred, desc):
|
|
26
|
+
deadline = time.time() + self._to / 1000.0
|
|
27
|
+
last = None
|
|
28
|
+
while True:
|
|
29
|
+
try:
|
|
30
|
+
last = bool(pred())
|
|
31
|
+
except (HelixwrightError, TypeError, AttributeError):
|
|
32
|
+
# transient (element gone / null evaluate) -> failed-but-retryable, never crash
|
|
33
|
+
last = False
|
|
34
|
+
if last != self._not:
|
|
35
|
+
return
|
|
36
|
+
if time.time() >= deadline:
|
|
37
|
+
raise AssertionError("expect %s%s failed (last=%r)"
|
|
38
|
+
% ("not_." if self._not else "", desc, last))
|
|
39
|
+
time.sleep(0.08)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class LocatorAssertions(_AssertionsBase):
|
|
43
|
+
"""Assertions about a Locator — visibility, text, value, count, state."""
|
|
44
|
+
|
|
45
|
+
def to_be_visible(self):
|
|
46
|
+
self._poll(self._t.is_visible, "to_be_visible")
|
|
47
|
+
|
|
48
|
+
def to_be_hidden(self):
|
|
49
|
+
self._poll(lambda: not self._t.is_visible(), "to_be_hidden")
|
|
50
|
+
|
|
51
|
+
def to_be_enabled(self):
|
|
52
|
+
self._poll(self._t.is_enabled, "to_be_enabled")
|
|
53
|
+
|
|
54
|
+
def to_be_disabled(self):
|
|
55
|
+
self._poll(lambda: not self._t.is_enabled(), "to_be_disabled")
|
|
56
|
+
|
|
57
|
+
def to_have_text(self, txt):
|
|
58
|
+
self._poll(lambda: self._t.text().strip() == txt, "to_have_text(%r)" % txt)
|
|
59
|
+
|
|
60
|
+
def to_contain_text(self, txt):
|
|
61
|
+
self._poll(lambda: txt in self._t.text(), "to_contain_text(%r)" % txt)
|
|
62
|
+
|
|
63
|
+
def to_have_value(self, v):
|
|
64
|
+
self._poll(lambda: self._t.input_value() == v, "to_have_value(%r)" % v)
|
|
65
|
+
|
|
66
|
+
def to_have_count(self, n):
|
|
67
|
+
self._poll(lambda: self._t.count() == n, "to_have_count(%d)" % n)
|
|
68
|
+
|
|
69
|
+
def to_be_checked(self):
|
|
70
|
+
self._poll(self._t.is_checked, "to_be_checked")
|
|
71
|
+
|
|
72
|
+
def to_be_editable(self):
|
|
73
|
+
self._poll(self._t.is_editable, "to_be_editable")
|
|
74
|
+
|
|
75
|
+
def to_be_focused(self):
|
|
76
|
+
self._poll(self._t.is_focused, "to_be_focused")
|
|
77
|
+
|
|
78
|
+
def to_have_attribute(self, name, value=None):
|
|
79
|
+
def _p():
|
|
80
|
+
v = self._t.get_attribute(name)
|
|
81
|
+
return (v is not None) if value is None else (v == value)
|
|
82
|
+
self._poll(_p, "to_have_attribute(%r,%r)" % (name, value))
|
|
83
|
+
|
|
84
|
+
# --- Playwright-parity matchers (pure-Python over Locator.evaluate/get_attribute/count) ----
|
|
85
|
+
def to_have_class(self, value):
|
|
86
|
+
"""The full class attribute equals |value| (Playwright string form)."""
|
|
87
|
+
self._poll(lambda: (self._t.get_attribute("class") or "") == value, "to_have_class(%r)" % value)
|
|
88
|
+
|
|
89
|
+
def to_contain_class(self, value):
|
|
90
|
+
"""The class token list contains |value|."""
|
|
91
|
+
self._poll(lambda: value in (self._t.get_attribute("class") or "").split(),
|
|
92
|
+
"to_contain_class(%r)" % value)
|
|
93
|
+
|
|
94
|
+
def to_have_id(self, id_):
|
|
95
|
+
self._poll(lambda: (self._t.get_attribute("id") or "") == id_, "to_have_id(%r)" % id_)
|
|
96
|
+
|
|
97
|
+
def to_have_css(self, name, value):
|
|
98
|
+
# [no-JS] native WebElement::GetComputedValue via Locator.computed_style.
|
|
99
|
+
self._poll(lambda: self._t.computed_style(name) == value, "to_have_css(%r,%r)" % (name, value))
|
|
100
|
+
|
|
101
|
+
def to_have_js_property(self, name, value):
|
|
102
|
+
self._poll(lambda: self._t.evaluate("el[%s]" % json.dumps(name)) == value,
|
|
103
|
+
"to_have_js_property(%r,%r)" % (name, value))
|
|
104
|
+
|
|
105
|
+
def to_have_values(self, values):
|
|
106
|
+
"""The selected <option> values of a multi-select equal |values| (list)."""
|
|
107
|
+
want = list(values)
|
|
108
|
+
self._poll(lambda: self._t.evaluate(
|
|
109
|
+
"Array.prototype.map.call(el.selectedOptions||[],function(o){return o.value;})") == want,
|
|
110
|
+
"to_have_values(%r)" % (want,))
|
|
111
|
+
|
|
112
|
+
def to_be_empty(self):
|
|
113
|
+
"""No child elements and no text (Playwright to_be_empty). [no-JS] native InnerHTML()=="" check."""
|
|
114
|
+
self._poll(lambda: (self._t.inner_html() or "").strip() == "", "to_be_empty")
|
|
115
|
+
|
|
116
|
+
def to_be_attached(self):
|
|
117
|
+
self._poll(lambda: self._t.count() > 0, "to_be_attached")
|
|
118
|
+
|
|
119
|
+
def to_be_in_viewport(self):
|
|
120
|
+
# [no-JS] VisibleBoundsInWidget() is viewport-clipped, so IsVisible() == visible-in-viewport.
|
|
121
|
+
self._poll(self._t.is_visible, "to_be_in_viewport")
|
|
122
|
+
|
|
123
|
+
def to_have_role(self, role):
|
|
124
|
+
"""APPROXIMATE computed ARIA role (explicit role attr or a tag heuristic — not the engine AOM).
|
|
125
|
+
[no-JS] via Locator._aria_role (get_attribute + element_property('tagName'))."""
|
|
126
|
+
self._poll(lambda: self._t._aria_role() == role, "to_have_role(%r)" % role)
|
|
127
|
+
|
|
128
|
+
def to_have_accessible_name(self, name):
|
|
129
|
+
"""APPROXIMATE accessible name (aria-label/text/alt/title heuristic). [no-JS] via native RPCs."""
|
|
130
|
+
self._poll(lambda: (self._t._accessible_name() or "").strip() == name,
|
|
131
|
+
"to_have_accessible_name(%r)" % name)
|
|
132
|
+
|
|
133
|
+
def to_have_accessible_description(self, desc):
|
|
134
|
+
"""APPROXIMATE accessible description (title heuristic). [no-JS] via native RPC."""
|
|
135
|
+
self._poll(lambda: (self._t._accessible_description() or "").strip() == desc,
|
|
136
|
+
"to_have_accessible_description(%r)" % desc)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class PageAssertions(_AssertionsBase):
|
|
140
|
+
"""Assertions about a Page — url, title."""
|
|
141
|
+
|
|
142
|
+
def _page(self):
|
|
143
|
+
# target is normally the Page; tolerate a Locator (resolve its page) for the
|
|
144
|
+
# combined Expect's back-compat.
|
|
145
|
+
t = self._t
|
|
146
|
+
return t if hasattr(t, "goto") else getattr(t, "_page", t)
|
|
147
|
+
|
|
148
|
+
def to_have_url(self, url):
|
|
149
|
+
self._poll(lambda: url in self._page().evaluate("location.href"),
|
|
150
|
+
"to_have_url(%r)" % url)
|
|
151
|
+
|
|
152
|
+
def to_have_title(self, title):
|
|
153
|
+
self._poll(lambda: title in self._page().evaluate("document.title"),
|
|
154
|
+
"to_have_title(%r)" % title)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
class Expect(LocatorAssertions, PageAssertions):
|
|
158
|
+
"""Back-compat combined assertions (the public ``Expect`` export). Prefer ``expect()``,
|
|
159
|
+
which returns the precise LocatorAssertions / PageAssertions for the target."""
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def expect(target, timeout_ms=5000):
|
|
163
|
+
"""Polling assertion proxy for |target|: a Page -> PageAssertions (url/title), a
|
|
164
|
+
Locator -> LocatorAssertions (visibility/text/value/state). Async facade objects
|
|
165
|
+
(AsyncPage/AsyncLocator) are unwrapped to their sync object (assertions are
|
|
166
|
+
synchronous). For a bare Element use page.locator(css) instead."""
|
|
167
|
+
inner = getattr(target, "_p", None) # AsyncPage wraps the sync Page as _p
|
|
168
|
+
if inner is None:
|
|
169
|
+
inner = getattr(target, "_l", None) # AsyncLocator wraps the sync Locator as _l
|
|
170
|
+
if inner is not None:
|
|
171
|
+
target = inner
|
|
172
|
+
if hasattr(target, "goto"): # a Page
|
|
173
|
+
return PageAssertions(target, timeout_ms)
|
|
174
|
+
# Locator OR Element -> BOTH expose _as_locator() (Locator returns self; Element re-resolves to a
|
|
175
|
+
# Locator from its originating selector, or raises clearly for a selector-less Element). Resolve to
|
|
176
|
+
# a Locator so assertions POLL across re-render/detach (a fixed node_id snapshot can't survive one).
|
|
177
|
+
# We no longer inspect the .text descriptor to tell Element from Locator: that property-vs-method
|
|
178
|
+
# hack breaks once the two unify, and an instance .text read would fire a get_text RPC on a
|
|
179
|
+
# navigating page. _as_locator() is a plain method (no RPC) present on both.
|
|
180
|
+
loc_fn = getattr(target, "_as_locator", None)
|
|
181
|
+
if callable(loc_fn):
|
|
182
|
+
return LocatorAssertions(loc_fn(), timeout_ms)
|
|
183
|
+
raise HelixwrightError(
|
|
184
|
+
"expect() needs a Page, Locator, or selector-based Element; got %r" % type(target).__name__)
|