allwright 0.0.1__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.
- allwright-0.0.1/PKG-INFO +34 -0
- allwright-0.0.1/README.md +23 -0
- allwright-0.0.1/allwright/__init__.py +69 -0
- allwright-0.0.1/allwright/_browser.py +137 -0
- allwright-0.0.1/allwright/_config.py +203 -0
- allwright-0.0.1/allwright/_locator.py +62 -0
- allwright-0.0.1/allwright/_page.py +534 -0
- allwright-0.0.1/allwright/_proto.py +27 -0
- allwright-0.0.1/allwright/_runtime.py +109 -0
- allwright-0.0.1/allwright/_selectors.py +48 -0
- allwright-0.0.1/allwright/_transport.py +52 -0
- allwright-0.0.1/allwright/_types.py +136 -0
- allwright-0.0.1/allwright/client.py +75 -0
- allwright-0.0.1/allwright/proto/core/v1/browser.proto +57 -0
- allwright-0.0.1/allwright/proto/core/v1/common.proto +15 -0
- allwright-0.0.1/allwright/proto/core/v1/tab.proto +72 -0
- allwright-0.0.1/allwright/proto/engine/v1/engine.proto +14 -0
- allwright-0.0.1/allwright/proto/surfaces/web/v1/web.proto +170 -0
- allwright-0.0.1/allwright.egg-info/PKG-INFO +34 -0
- allwright-0.0.1/allwright.egg-info/SOURCES.txt +23 -0
- allwright-0.0.1/allwright.egg-info/dependency_links.txt +1 -0
- allwright-0.0.1/allwright.egg-info/requires.txt +4 -0
- allwright-0.0.1/allwright.egg-info/top_level.txt +1 -0
- allwright-0.0.1/pyproject.toml +26 -0
- allwright-0.0.1/setup.cfg +4 -0
allwright-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: allwright
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: High-level Python client for the allwright engine
|
|
5
|
+
Requires-Python: >=3.10
|
|
6
|
+
Description-Content-Type: text/markdown
|
|
7
|
+
Requires-Dist: grpcio<2,>=1.74.0
|
|
8
|
+
Requires-Dist: grpcio-tools<2,>=1.74.0
|
|
9
|
+
Requires-Dist: protobuf<7,>=5
|
|
10
|
+
Requires-Dist: PyYAML<7,>=6
|
|
11
|
+
|
|
12
|
+
# allwright Python client
|
|
13
|
+
|
|
14
|
+
This folder contains the high-level Python client for the allwright engine.
|
|
15
|
+
|
|
16
|
+
It loads the shared `proto/engine/v1/engine.proto` contract dynamically at runtime and exposes a browser/page API instead of raw gRPC channel setup.
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install -e ./python
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from allwright import firefox
|
|
28
|
+
|
|
29
|
+
browser = firefox.launch()
|
|
30
|
+
page = browser.page()
|
|
31
|
+
page.goto("https://example.com")
|
|
32
|
+
page.click("h1")
|
|
33
|
+
browser.close()
|
|
34
|
+
```
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# allwright Python client
|
|
2
|
+
|
|
3
|
+
This folder contains the high-level Python client for the allwright engine.
|
|
4
|
+
|
|
5
|
+
It loads the shared `proto/engine/v1/engine.proto` contract dynamically at runtime and exposes a browser/page API instead of raw gRPC channel setup.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install -e ./python
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Example
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from allwright import firefox
|
|
17
|
+
|
|
18
|
+
browser = firefox.launch()
|
|
19
|
+
page = browser.page()
|
|
20
|
+
page.goto("https://example.com")
|
|
21
|
+
page.click("h1")
|
|
22
|
+
browser.close()
|
|
23
|
+
```
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from .client import (
|
|
2
|
+
Browser,
|
|
3
|
+
BrowserType,
|
|
4
|
+
ClickResult,
|
|
5
|
+
CommandOptions,
|
|
6
|
+
CountResult,
|
|
7
|
+
ElementResult,
|
|
8
|
+
FillResult,
|
|
9
|
+
HighlightOptions,
|
|
10
|
+
HighlightResult,
|
|
11
|
+
LaunchOptions,
|
|
12
|
+
Locator,
|
|
13
|
+
AllwrightConfig,
|
|
14
|
+
NavigateResult,
|
|
15
|
+
Page,
|
|
16
|
+
PressOptions,
|
|
17
|
+
PressResult,
|
|
18
|
+
ResolveConfigOptions,
|
|
19
|
+
ResolvedConfig,
|
|
20
|
+
TextResult,
|
|
21
|
+
WaitForSelectorOptions,
|
|
22
|
+
WaitForSelectorResult,
|
|
23
|
+
chromium,
|
|
24
|
+
find_config_file,
|
|
25
|
+
firefox,
|
|
26
|
+
launch_chrome,
|
|
27
|
+
launch_configured_browser,
|
|
28
|
+
launch_firefox,
|
|
29
|
+
load_config_file,
|
|
30
|
+
ping,
|
|
31
|
+
resolve_config,
|
|
32
|
+
set_server_addr,
|
|
33
|
+
shutdown,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"Browser",
|
|
38
|
+
"BrowserType",
|
|
39
|
+
"ClickResult",
|
|
40
|
+
"CommandOptions",
|
|
41
|
+
"CountResult",
|
|
42
|
+
"ElementResult",
|
|
43
|
+
"FillResult",
|
|
44
|
+
"HighlightOptions",
|
|
45
|
+
"HighlightResult",
|
|
46
|
+
"LaunchOptions",
|
|
47
|
+
"Locator",
|
|
48
|
+
"AllwrightConfig",
|
|
49
|
+
"NavigateResult",
|
|
50
|
+
"Page",
|
|
51
|
+
"PressOptions",
|
|
52
|
+
"PressResult",
|
|
53
|
+
"ResolveConfigOptions",
|
|
54
|
+
"ResolvedConfig",
|
|
55
|
+
"TextResult",
|
|
56
|
+
"WaitForSelectorOptions",
|
|
57
|
+
"WaitForSelectorResult",
|
|
58
|
+
"chromium",
|
|
59
|
+
"find_config_file",
|
|
60
|
+
"firefox",
|
|
61
|
+
"launch_chrome",
|
|
62
|
+
"launch_configured_browser",
|
|
63
|
+
"launch_firefox",
|
|
64
|
+
"load_config_file",
|
|
65
|
+
"ping",
|
|
66
|
+
"resolve_config",
|
|
67
|
+
"set_server_addr",
|
|
68
|
+
"shutdown",
|
|
69
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
|
|
5
|
+
from ._proto import engine_pb2
|
|
6
|
+
from ._types import AllwrightError, CommandOptions, LaunchOptions
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class BrowserType:
|
|
10
|
+
def __init__(self, browser_kind: int = engine_pb2.BROWSER_KIND_CHROMIUM) -> None:
|
|
11
|
+
self._browser_kind = browser_kind
|
|
12
|
+
|
|
13
|
+
def launch(self, options: LaunchOptions | None = None) -> Browser:
|
|
14
|
+
from .client import launch_browser
|
|
15
|
+
|
|
16
|
+
return launch_browser(self._browser_kind, options or LaunchOptions())
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class Browser:
|
|
20
|
+
def __init__(
|
|
21
|
+
self,
|
|
22
|
+
runtime: RuntimeClient,
|
|
23
|
+
stream: StreamHandle,
|
|
24
|
+
session_id: str,
|
|
25
|
+
browser_name: str,
|
|
26
|
+
launch_note: str,
|
|
27
|
+
cdp_websocket_url: str,
|
|
28
|
+
user_data_dir: str,
|
|
29
|
+
initial_page: Page,
|
|
30
|
+
) -> None:
|
|
31
|
+
self._runtime = runtime
|
|
32
|
+
self._stream = stream
|
|
33
|
+
self._lock = threading.Lock()
|
|
34
|
+
self._closed = False
|
|
35
|
+
self._pages: dict[str, Page] = {initial_page.session_id: initial_page}
|
|
36
|
+
self.session_id = session_id
|
|
37
|
+
self.browser_name = browser_name
|
|
38
|
+
self.launch_note = launch_note
|
|
39
|
+
self.cdp_websocket_url = cdp_websocket_url
|
|
40
|
+
self.user_data_dir = user_data_dir
|
|
41
|
+
self._initial_page = initial_page
|
|
42
|
+
|
|
43
|
+
def page(self) -> Page:
|
|
44
|
+
return self._initial_page
|
|
45
|
+
|
|
46
|
+
def initial_page(self) -> Page:
|
|
47
|
+
return self._initial_page
|
|
48
|
+
|
|
49
|
+
def initial_tab(self) -> Page:
|
|
50
|
+
return self._initial_page
|
|
51
|
+
|
|
52
|
+
def pages(self) -> list[Page]:
|
|
53
|
+
return list(self._pages.values())
|
|
54
|
+
|
|
55
|
+
def new_page(self, options: CommandOptions | None = None) -> Page:
|
|
56
|
+
from ._page import Page
|
|
57
|
+
from ._runtime import retry_options
|
|
58
|
+
|
|
59
|
+
with self._lock:
|
|
60
|
+
self._ensure_open()
|
|
61
|
+
command_options = options or CommandOptions()
|
|
62
|
+
self._stream.send(
|
|
63
|
+
engine_pb2.BrowserSessionCommand(
|
|
64
|
+
open_tab=engine_pb2.OpenTabCommand(
|
|
65
|
+
retry_options=retry_options(command_options.timeout_ms),
|
|
66
|
+
),
|
|
67
|
+
)
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
while True:
|
|
71
|
+
event = self._stream.recv("receive browser session event while opening page")
|
|
72
|
+
match event.WhichOneof("event"):
|
|
73
|
+
case "tab_opened":
|
|
74
|
+
page = Page(
|
|
75
|
+
runtime=self._runtime,
|
|
76
|
+
browser_session_id=self.session_id,
|
|
77
|
+
session_id=event.tab_opened.tab_session_id,
|
|
78
|
+
)
|
|
79
|
+
self._pages[page.session_id] = page
|
|
80
|
+
return page
|
|
81
|
+
case "error":
|
|
82
|
+
raise AllwrightError(
|
|
83
|
+
f"browser session error while opening page: {event.error.message}"
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def new_tab(self, options: CommandOptions | None = None) -> Page:
|
|
87
|
+
return self.new_page(options)
|
|
88
|
+
|
|
89
|
+
def ping(self, message: str = "ping") -> str:
|
|
90
|
+
with self._lock:
|
|
91
|
+
self._ensure_open()
|
|
92
|
+
self._stream.send(
|
|
93
|
+
engine_pb2.BrowserSessionCommand(
|
|
94
|
+
ping=engine_pb2.SessionPingCommand(message=message),
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
while True:
|
|
99
|
+
event = self._stream.recv("receive browser session event while pinging browser")
|
|
100
|
+
match event.WhichOneof("event"):
|
|
101
|
+
case "pong":
|
|
102
|
+
return event.pong.message
|
|
103
|
+
case "error":
|
|
104
|
+
raise AllwrightError(
|
|
105
|
+
f"browser session error while pinging: {event.error.message}"
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
def close(self) -> None:
|
|
109
|
+
with self._lock:
|
|
110
|
+
if self._closed:
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
self._stream.send(
|
|
114
|
+
engine_pb2.BrowserSessionCommand(
|
|
115
|
+
close=engine_pb2.CloseBrowserSessionCommand(),
|
|
116
|
+
)
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
while True:
|
|
120
|
+
event = self._stream.recv("receive browser session event while closing browser")
|
|
121
|
+
match event.WhichOneof("event"):
|
|
122
|
+
case "closed":
|
|
123
|
+
self._closed = True
|
|
124
|
+
self._stream.close_send()
|
|
125
|
+
return
|
|
126
|
+
case "error":
|
|
127
|
+
raise AllwrightError(
|
|
128
|
+
f"browser session error while closing: {event.error.message}"
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
def _ensure_open(self) -> None:
|
|
132
|
+
if self._closed:
|
|
133
|
+
raise AllwrightError(f"browser session {self.session_id} is closed")
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
from ._page import Page
|
|
137
|
+
from ._transport import RuntimeClient, StreamHandle
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
from ._types import (
|
|
10
|
+
AllwrightConfig,
|
|
11
|
+
AllwrightError,
|
|
12
|
+
LaunchOptions,
|
|
13
|
+
ResolveConfigOptions,
|
|
14
|
+
ResolvedConfig,
|
|
15
|
+
RetryConfig,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
CONFIG_FILENAMES = (
|
|
19
|
+
"allwright.config.yaml",
|
|
20
|
+
"allwright.config.yml",
|
|
21
|
+
"allwright.config.json",
|
|
22
|
+
".allwright/config.yaml",
|
|
23
|
+
".allwright/config.yml",
|
|
24
|
+
".allwright/config.json",
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def find_config_file(start_dir: str | Path | None = None) -> Path | None:
|
|
29
|
+
current_dir = Path(start_dir or Path.cwd()).resolve()
|
|
30
|
+
|
|
31
|
+
while True:
|
|
32
|
+
for filename in CONFIG_FILENAMES:
|
|
33
|
+
candidate = current_dir / filename
|
|
34
|
+
if candidate.is_file():
|
|
35
|
+
return candidate
|
|
36
|
+
|
|
37
|
+
if current_dir.parent == current_dir:
|
|
38
|
+
return None
|
|
39
|
+
current_dir = current_dir.parent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def load_config_file(config_file: str | Path) -> AllwrightConfig:
|
|
43
|
+
resolved = Path(config_file).resolve()
|
|
44
|
+
raw = resolved.read_text(encoding="utf-8")
|
|
45
|
+
|
|
46
|
+
if resolved.suffix.lower() == ".json":
|
|
47
|
+
parsed = json.loads(raw)
|
|
48
|
+
elif resolved.suffix.lower() in {".yaml", ".yml"}:
|
|
49
|
+
parsed = yaml.safe_load(raw) or {}
|
|
50
|
+
else:
|
|
51
|
+
raise AllwrightError(
|
|
52
|
+
f"unsupported allwright config file extension {resolved.suffix or '<none>'} for {resolved}"
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if not isinstance(parsed, dict):
|
|
56
|
+
raise AllwrightError(f"allwright config {resolved} must contain a top-level object")
|
|
57
|
+
|
|
58
|
+
validate_config_shape(parsed, resolved)
|
|
59
|
+
return AllwrightConfig(
|
|
60
|
+
schema_version=parsed.get("schemaVersion"),
|
|
61
|
+
server=parsed.get("server"),
|
|
62
|
+
browser=parsed.get("browser"),
|
|
63
|
+
expect=retry_config_from_mapping(parsed.get("expect")),
|
|
64
|
+
suites=parsed.get("suites"),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def resolve_config(options: ResolveConfigOptions | None = None) -> ResolvedConfig:
|
|
69
|
+
resolved_options = options or ResolveConfigOptions()
|
|
70
|
+
config_file_path = (
|
|
71
|
+
Path(resolved_options.config_file).resolve()
|
|
72
|
+
if resolved_options.config_file
|
|
73
|
+
else find_config_file(resolved_options.cwd)
|
|
74
|
+
)
|
|
75
|
+
file_config = load_config_file(config_file_path) if config_file_path else AllwrightConfig()
|
|
76
|
+
suite_name = (resolved_options.suite or "").strip() or None
|
|
77
|
+
suite_config = None
|
|
78
|
+
|
|
79
|
+
if suite_name:
|
|
80
|
+
suite_config = (file_config.suites or {}).get(suite_name)
|
|
81
|
+
if suite_config is None:
|
|
82
|
+
source = str(config_file_path) if config_file_path else "the resolved config file"
|
|
83
|
+
raise AllwrightError(
|
|
84
|
+
f'allwright config suite "{suite_name}" was not found in {source}'
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
server_addr = first_non_empty(
|
|
88
|
+
server_addr_from_mapping(suite_config.get("server") if suite_config else None),
|
|
89
|
+
server_addr_from_mapping(file_config.server),
|
|
90
|
+
)
|
|
91
|
+
browser_name = first_non_empty(
|
|
92
|
+
browser_name_from_mapping(suite_config.get("browser") if suite_config else None),
|
|
93
|
+
browser_name_from_mapping(file_config.browser),
|
|
94
|
+
) or "chromium"
|
|
95
|
+
browser_binary = first_non_empty(
|
|
96
|
+
browser_binary_from_mapping(suite_config.get("browser") if suite_config else None),
|
|
97
|
+
browser_binary_from_mapping(file_config.browser),
|
|
98
|
+
)
|
|
99
|
+
launch_options = merge_launch_options(
|
|
100
|
+
launch_options_from_mapping(file_config.browser),
|
|
101
|
+
launch_options_from_mapping(suite_config.get("browser") if suite_config else None),
|
|
102
|
+
)
|
|
103
|
+
if browser_binary:
|
|
104
|
+
launch_options.browser_binary = browser_binary
|
|
105
|
+
expect = merge_retry_config(
|
|
106
|
+
file_config.expect,
|
|
107
|
+
retry_config_from_mapping(suite_config.get("expect") if suite_config else None),
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
return ResolvedConfig(
|
|
111
|
+
config_file_path=config_file_path,
|
|
112
|
+
suite_name=suite_name,
|
|
113
|
+
server_addr=server_addr,
|
|
114
|
+
browser_name=browser_name,
|
|
115
|
+
browser_binary=browser_binary,
|
|
116
|
+
launch_options=launch_options,
|
|
117
|
+
expect=expect,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def validate_config_shape(config: dict[str, Any], source: Path) -> None:
|
|
122
|
+
schema_version = config.get("schemaVersion")
|
|
123
|
+
if schema_version is not None and schema_version != 1:
|
|
124
|
+
raise AllwrightError(
|
|
125
|
+
f"allwright config {source} has unsupported schemaVersion {schema_version}; expected 1"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
browser_name = browser_name_from_mapping(config.get("browser"))
|
|
129
|
+
if browser_name and browser_name not in {"chromium", "firefox"}:
|
|
130
|
+
raise AllwrightError(
|
|
131
|
+
f'allwright config {source} has unsupported browser.name "{browser_name}"; use "chromium" or "firefox"'
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def browser_name_from_mapping(browser: dict[str, Any] | None) -> str | None:
|
|
136
|
+
if not isinstance(browser, dict):
|
|
137
|
+
return None
|
|
138
|
+
value = browser.get("name")
|
|
139
|
+
return value.strip() if isinstance(value, str) and value.strip() else None
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def browser_binary_from_mapping(browser: dict[str, Any] | None) -> str | None:
|
|
143
|
+
if not isinstance(browser, dict):
|
|
144
|
+
return None
|
|
145
|
+
value = browser.get("binary")
|
|
146
|
+
return value.strip() if isinstance(value, str) and value.strip() else None
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def server_addr_from_mapping(server: dict[str, Any] | None) -> str | None:
|
|
150
|
+
if not isinstance(server, dict):
|
|
151
|
+
return None
|
|
152
|
+
value = server.get("addr")
|
|
153
|
+
return value.strip() if isinstance(value, str) and value.strip() else None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def launch_options_from_mapping(browser: dict[str, Any] | None) -> LaunchOptions:
|
|
157
|
+
if not isinstance(browser, dict):
|
|
158
|
+
return LaunchOptions()
|
|
159
|
+
launch_options = browser.get("launchOptions")
|
|
160
|
+
if not isinstance(launch_options, dict):
|
|
161
|
+
return LaunchOptions()
|
|
162
|
+
return LaunchOptions(
|
|
163
|
+
browser_binary=launch_options.get("browserBinary"),
|
|
164
|
+
timeout_ms=launch_options.get("timeoutMs"),
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def retry_config_from_mapping(value: dict[str, Any] | None) -> RetryConfig | None:
|
|
169
|
+
if not isinstance(value, dict):
|
|
170
|
+
return None
|
|
171
|
+
return RetryConfig(
|
|
172
|
+
timeout_ms=value.get("timeoutMs"),
|
|
173
|
+
interval_ms=value.get("intervalMs"),
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def merge_launch_options(base: LaunchOptions, override: LaunchOptions) -> LaunchOptions:
|
|
178
|
+
return LaunchOptions(
|
|
179
|
+
browser_binary=override.browser_binary or base.browser_binary,
|
|
180
|
+
timeout_ms=override.timeout_ms if override.timeout_ms is not None else base.timeout_ms,
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def merge_retry_config(base: RetryConfig | None, override: RetryConfig | None) -> RetryConfig:
|
|
185
|
+
return RetryConfig(
|
|
186
|
+
timeout_ms=(
|
|
187
|
+
override.timeout_ms
|
|
188
|
+
if override and override.timeout_ms is not None
|
|
189
|
+
else (base.timeout_ms if base else None)
|
|
190
|
+
),
|
|
191
|
+
interval_ms=(
|
|
192
|
+
override.interval_ms
|
|
193
|
+
if override and override.interval_ms is not None
|
|
194
|
+
else (base.interval_ms if base else None)
|
|
195
|
+
),
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def first_non_empty(*values: str | None) -> str | None:
|
|
200
|
+
for value in values:
|
|
201
|
+
if isinstance(value, str) and value.strip():
|
|
202
|
+
return value.strip()
|
|
203
|
+
return None
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING
|
|
5
|
+
|
|
6
|
+
from ._selectors import chain_selector_for_transport
|
|
7
|
+
from ._types import (
|
|
8
|
+
ClickResult,
|
|
9
|
+
CommandOptions,
|
|
10
|
+
CountResult,
|
|
11
|
+
ElementResult,
|
|
12
|
+
FillResult,
|
|
13
|
+
HighlightOptions,
|
|
14
|
+
HighlightResult,
|
|
15
|
+
PressOptions,
|
|
16
|
+
PressResult,
|
|
17
|
+
TextResult,
|
|
18
|
+
WaitForSelectorOptions,
|
|
19
|
+
WaitForSelectorResult,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
if TYPE_CHECKING:
|
|
23
|
+
from ._page import Page
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(slots=True)
|
|
27
|
+
class Locator:
|
|
28
|
+
page: Page
|
|
29
|
+
selector: str
|
|
30
|
+
|
|
31
|
+
def locator(self, selector: str) -> Locator:
|
|
32
|
+
return Locator(page=self.page, selector=chain_selector_for_transport(self.selector, selector))
|
|
33
|
+
|
|
34
|
+
def click(self, options: CommandOptions | None = None) -> ClickResult:
|
|
35
|
+
return self.page.click(self.selector, options)
|
|
36
|
+
|
|
37
|
+
def count(self, options: CommandOptions | None = None) -> CountResult:
|
|
38
|
+
return self.page.count(self.selector, options)
|
|
39
|
+
|
|
40
|
+
def highlight(self, options: HighlightOptions | None = None) -> HighlightResult:
|
|
41
|
+
return self.page.highlight(self.selector, options)
|
|
42
|
+
|
|
43
|
+
def focus(self, options: CommandOptions | None = None) -> ElementResult:
|
|
44
|
+
return self.page.focus(self.selector, options)
|
|
45
|
+
|
|
46
|
+
def fill(self, value: str, options: CommandOptions | None = None) -> FillResult:
|
|
47
|
+
return self.page.fill(self.selector, value, options)
|
|
48
|
+
|
|
49
|
+
def hover(self, options: CommandOptions | None = None) -> ElementResult:
|
|
50
|
+
return self.page.hover(self.selector, options)
|
|
51
|
+
|
|
52
|
+
def press(self, key: str, options: PressOptions | None = None) -> PressResult:
|
|
53
|
+
return self.page.press(self.selector, key, options)
|
|
54
|
+
|
|
55
|
+
def text_content(self, options: CommandOptions | None = None) -> TextResult:
|
|
56
|
+
return self.page.text_content(self.selector, options)
|
|
57
|
+
|
|
58
|
+
def inner_text(self, options: CommandOptions | None = None) -> TextResult:
|
|
59
|
+
return self.page.inner_text(self.selector, options)
|
|
60
|
+
|
|
61
|
+
def wait_for(self, options: WaitForSelectorOptions | None = None) -> WaitForSelectorResult:
|
|
62
|
+
return self.page.wait_for_selector(self.selector, options)
|