qplay 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.
- qplay-0.1/PKG-INFO +33 -0
- qplay-0.1/qplay/__init__.py +84 -0
- qplay-0.1/qplay/identylib.py +492 -0
- qplay-0.1/qplay/iframepiercer.py +789 -0
- qplay-0.1/qplay/interface.py +965 -0
- qplay-0.1/qplay/kernel.py +571 -0
- qplay-0.1/qplay/persist.py +202 -0
- qplay-0.1/qplay/persistproc.py +737 -0
- qplay-0.1/qplay/play.py +3021 -0
- qplay-0.1/qplay/ptable.py +728 -0
- qplay-0.1/qplay/pula.py +1048 -0
- qplay-0.1/qplay/qweb_ui.py +1727 -0
- qplay-0.1/qplay/qweb_ui2.py +802 -0
- qplay-0.1/qplay/qweb_ui_example.py +43 -0
- qplay-0.1/qplay/stdio.py +264 -0
- qplay-0.1/qplay/test.py +57 -0
- qplay-0.1/qplay/test_autoload.py +51 -0
- qplay-0.1/qplay/test_dual_process.py +180 -0
- qplay-0.1/qplay/test_suite.py +369 -0
- qplay-0.1/qplay/utils.py +133 -0
- qplay-0.1/qplay/web_daemon.py +473 -0
- qplay-0.1/qplay/web_process.py +745 -0
- qplay-0.1/qplay.egg-info/PKG-INFO +33 -0
- qplay-0.1/qplay.egg-info/SOURCES.txt +27 -0
- qplay-0.1/qplay.egg-info/dependency_links.txt +1 -0
- qplay-0.1/qplay.egg-info/requires.txt +12 -0
- qplay-0.1/qplay.egg-info/top_level.txt +1 -0
- qplay-0.1/setup.cfg +4 -0
- qplay-0.1/setup.py +50 -0
qplay-0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: qplay
|
|
3
|
+
Version: 0.1
|
|
4
|
+
Summary: ...
|
|
5
|
+
Author-email: 2229066748@qq.com
|
|
6
|
+
Maintainer: Eagle'sBaby
|
|
7
|
+
Maintainer-email: 2229066748@qq.com
|
|
8
|
+
License: Apache Licence 2.0
|
|
9
|
+
Keywords: pyqt
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Requires-Dist: colorama
|
|
14
|
+
Requires-Dist: files3>=0.10
|
|
15
|
+
Requires-Dist: imageio
|
|
16
|
+
Requires-Dist: loguru
|
|
17
|
+
Requires-Dist: lxml
|
|
18
|
+
Requires-Dist: markdown
|
|
19
|
+
Requires-Dist: numpy
|
|
20
|
+
Requires-Dist: tpltable>=0.5
|
|
21
|
+
Requires-Dist: playwright
|
|
22
|
+
Requires-Dist: pyautogui
|
|
23
|
+
Requires-Dist: PyQt6
|
|
24
|
+
Requires-Dist: tqdm
|
|
25
|
+
Dynamic: author-email
|
|
26
|
+
Dynamic: classifier
|
|
27
|
+
Dynamic: keywords
|
|
28
|
+
Dynamic: license
|
|
29
|
+
Dynamic: maintainer
|
|
30
|
+
Dynamic: maintainer-email
|
|
31
|
+
Dynamic: requires-dist
|
|
32
|
+
Dynamic: requires-python
|
|
33
|
+
Dynamic: summary
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from typing import Any
|
|
2
|
+
|
|
3
|
+
from .kernel import (
|
|
4
|
+
GlobalState,
|
|
5
|
+
FilePathRealer,
|
|
6
|
+
Kernel,
|
|
7
|
+
K
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
from .utils import (
|
|
11
|
+
DownloadWatcher,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
from .identylib import (
|
|
15
|
+
IdentityLib,
|
|
16
|
+
IdentityRecord,
|
|
17
|
+
ID,
|
|
18
|
+
Identity,
|
|
19
|
+
Register,
|
|
20
|
+
Resolve,
|
|
21
|
+
Batch,
|
|
22
|
+
MACRO_PREFIX,
|
|
23
|
+
ID_PREFIX,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
from .play import (
|
|
27
|
+
Element,
|
|
28
|
+
WebPage,
|
|
29
|
+
initplay,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
from .ptable import (
|
|
33
|
+
WebTable,
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
from .persist import (
|
|
37
|
+
QWeb,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
# Direct component access (from initialized kernel)
|
|
42
|
+
G = K.G
|
|
43
|
+
F = K.F
|
|
44
|
+
H = K.H
|
|
45
|
+
T = K.T
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
__all__ = [
|
|
50
|
+
# Core components
|
|
51
|
+
'G', 'F', 'H', 'T', 'K',
|
|
52
|
+
|
|
53
|
+
# Kernel classes
|
|
54
|
+
'Kernel',
|
|
55
|
+
'GlobalState',
|
|
56
|
+
'FilePathRealer',
|
|
57
|
+
|
|
58
|
+
# Utils
|
|
59
|
+
'DownloadWatcher',
|
|
60
|
+
|
|
61
|
+
# Identity
|
|
62
|
+
'IdentityLib',
|
|
63
|
+
'IdentityRecord',
|
|
64
|
+
'ID',
|
|
65
|
+
'Identity',
|
|
66
|
+
'Register',
|
|
67
|
+
'Resolve',
|
|
68
|
+
'Batch',
|
|
69
|
+
'MACRO_PREFIX',
|
|
70
|
+
'ID_PREFIX',
|
|
71
|
+
|
|
72
|
+
# Playwright browser automation
|
|
73
|
+
'Element',
|
|
74
|
+
'WebPage',
|
|
75
|
+
'initplay',
|
|
76
|
+
|
|
77
|
+
# Persistent Qt web container
|
|
78
|
+
'QWeb',
|
|
79
|
+
|
|
80
|
+
# Table wrapper
|
|
81
|
+
'WebTable',
|
|
82
|
+
|
|
83
|
+
]
|
|
84
|
+
|
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"""
|
|
2
|
+
qplay identylib module - Identity library management
|
|
3
|
+
|
|
4
|
+
Manages XPath-based element identifiers with macro support.
|
|
5
|
+
Provides centralized storage and lookup for element identities.
|
|
6
|
+
|
|
7
|
+
Identity format:
|
|
8
|
+
- xpath: "//div[@id='main']"
|
|
9
|
+
- @id: "@login" -> resolves to xpath "//*[@id='login']"
|
|
10
|
+
- $macro: "$login_btn" -> resolves to stored xpath
|
|
11
|
+
- inline def: "$btn: //button" -> registers and returns xpath
|
|
12
|
+
- parameterized: "$btn('submit')" -> resolves with format substitution
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import re
|
|
17
|
+
import json
|
|
18
|
+
import threading
|
|
19
|
+
from typing import Self
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
|
|
22
|
+
from .stdio import Info, Warn, Error
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
MACRO_PREFIX = "$"
|
|
26
|
+
ID_PREFIX = "@"
|
|
27
|
+
IDENTITY_FILE = "identities.json"
|
|
28
|
+
|
|
29
|
+
# Parameterized macro pattern: $Name('arg1', "arg2", ...)
|
|
30
|
+
_PARAM_PATTERN = re.compile(r"\$([^\(]+)\(([^)]+)\)")
|
|
31
|
+
# String literal pattern for parameter extraction
|
|
32
|
+
_STRING_PATTERN = re.compile(r"['\"][^'\"]*['\"]")
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@dataclass
|
|
36
|
+
class IdentityRecord:
|
|
37
|
+
"""Identity record with metadata."""
|
|
38
|
+
xpath: str
|
|
39
|
+
desc: str = ""
|
|
40
|
+
tag: str = ""
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class IdentityLib:
|
|
44
|
+
"""
|
|
45
|
+
Identity library for managing XPath-based element identifiers.
|
|
46
|
+
|
|
47
|
+
Supports macro definitions for shorthand access.
|
|
48
|
+
All identities are stored as XPath expressions.
|
|
49
|
+
|
|
50
|
+
Usage:
|
|
51
|
+
lib = IdentityLib()
|
|
52
|
+
lib.register("$login_btn", "//button[@id='login']", "Login button")
|
|
53
|
+
|
|
54
|
+
# Resolve different identity types
|
|
55
|
+
xpath = lib.resolve("$login_btn") # -> "//button[@id='login']"
|
|
56
|
+
xpath = lib.resolve("@login") # -> "//*[@id='login']"
|
|
57
|
+
xpath = lib.resolve("//div") # -> returns as-is
|
|
58
|
+
xpath = lib.resolve("$btn: //button") # registers and returns
|
|
59
|
+
xpath = lib.resolve("$btn('ok')") # -> "//button[text()='ok']"
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
_instance: "IdentityLib | None" = None
|
|
63
|
+
_initialized: bool = False
|
|
64
|
+
_lock: threading.Lock = threading.Lock()
|
|
65
|
+
|
|
66
|
+
def __new__(cls) -> "IdentityLib":
|
|
67
|
+
if cls._instance is None:
|
|
68
|
+
with cls._lock:
|
|
69
|
+
if cls._instance is None:
|
|
70
|
+
cls._instance = super().__new__(cls)
|
|
71
|
+
return cls._instance
|
|
72
|
+
|
|
73
|
+
def __init__(self):
|
|
74
|
+
with IdentityLib._lock:
|
|
75
|
+
if IdentityLib._initialized:
|
|
76
|
+
return
|
|
77
|
+
IdentityLib._initialized = True
|
|
78
|
+
|
|
79
|
+
self._macros: dict[str, IdentityRecord] = {}
|
|
80
|
+
self._storage_path: str | None = None
|
|
81
|
+
|
|
82
|
+
def init(self, storage_dir: str | None = None) -> Self:
|
|
83
|
+
"""
|
|
84
|
+
Initialize library with storage directory.
|
|
85
|
+
|
|
86
|
+
Args:
|
|
87
|
+
storage_dir: Directory for persistence, None for memory-only
|
|
88
|
+
|
|
89
|
+
Returns:
|
|
90
|
+
self for chaining
|
|
91
|
+
"""
|
|
92
|
+
if storage_dir is not None:
|
|
93
|
+
self._storage_path = os.path.join(storage_dir, IDENTITY_FILE)
|
|
94
|
+
self._load()
|
|
95
|
+
return self
|
|
96
|
+
|
|
97
|
+
def _load(self) -> None:
|
|
98
|
+
"""Load identities from storage."""
|
|
99
|
+
if self._storage_path is None or not os.path.exists(self._storage_path):
|
|
100
|
+
return
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
with open(self._storage_path, "r", encoding="utf-8") as f:
|
|
104
|
+
data = json.load(f)
|
|
105
|
+
for key, value in data.items():
|
|
106
|
+
if isinstance(value, str):
|
|
107
|
+
self._macros[key] = IdentityRecord(xpath=value)
|
|
108
|
+
elif isinstance(value, dict):
|
|
109
|
+
self._macros[key] = IdentityRecord(
|
|
110
|
+
xpath=value.get("xpath", ""),
|
|
111
|
+
desc=value.get("desc", ""),
|
|
112
|
+
tag=value.get("tag", "")
|
|
113
|
+
)
|
|
114
|
+
Info("idlib", "loaded", f"{len(self._macros)} macros")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
Warn("idlib", "load failed", str(e))
|
|
117
|
+
|
|
118
|
+
def save(self) -> Self:
|
|
119
|
+
"""
|
|
120
|
+
Save identities to storage.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
self for chaining
|
|
124
|
+
"""
|
|
125
|
+
if self._storage_path is None:
|
|
126
|
+
Warn("idlib", "no storage path set")
|
|
127
|
+
return self
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
data: dict[str, dict] = {}
|
|
131
|
+
for key, record in self._macros.items():
|
|
132
|
+
data[key] = {
|
|
133
|
+
"xpath": record.xpath,
|
|
134
|
+
"desc": record.desc,
|
|
135
|
+
"tag": record.tag
|
|
136
|
+
}
|
|
137
|
+
_dir = os.path.dirname(self._storage_path)
|
|
138
|
+
os.makedirs(_dir, exist_ok=True)
|
|
139
|
+
# Atomic write: temp file in same dir, then os.replace (atomic on
|
|
140
|
+
# same filesystem). Prevents a crash mid-write from leaving a
|
|
141
|
+
# truncated/corrupt identities store.
|
|
142
|
+
import tempfile
|
|
143
|
+
_fd, _tmp = tempfile.mkstemp(dir=_dir, suffix=".tmp")
|
|
144
|
+
try:
|
|
145
|
+
with os.fdopen(_fd, "w", encoding="utf-8") as f:
|
|
146
|
+
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
147
|
+
os.replace(_tmp, self._storage_path)
|
|
148
|
+
except Exception:
|
|
149
|
+
try:
|
|
150
|
+
os.remove(_tmp)
|
|
151
|
+
except OSError:
|
|
152
|
+
pass
|
|
153
|
+
raise
|
|
154
|
+
Info("idlib", "saved", f"{len(self._macros)} macros")
|
|
155
|
+
except Exception as e:
|
|
156
|
+
Error("idlib", "save failed", str(e))
|
|
157
|
+
return self
|
|
158
|
+
|
|
159
|
+
def register(self, macro: str, xpath: str, desc: str = "", tag: str = "") -> Self:
|
|
160
|
+
"""
|
|
161
|
+
Register a macro identity.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
macro: Macro name (auto-prefixed with $ if missing)
|
|
165
|
+
xpath: XPath expression
|
|
166
|
+
desc: Description
|
|
167
|
+
tag: Category tag
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
self for chaining
|
|
171
|
+
"""
|
|
172
|
+
if not macro.startswith(MACRO_PREFIX):
|
|
173
|
+
macro = MACRO_PREFIX + macro
|
|
174
|
+
|
|
175
|
+
self._macros[macro] = IdentityRecord(
|
|
176
|
+
xpath=xpath,
|
|
177
|
+
desc=desc,
|
|
178
|
+
tag=tag
|
|
179
|
+
)
|
|
180
|
+
Info("idlib", "registered", macro)
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def unregister(self, macro: str) -> bool:
|
|
184
|
+
"""
|
|
185
|
+
Unregister a macro.
|
|
186
|
+
|
|
187
|
+
Args:
|
|
188
|
+
macro: Macro name to remove
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
True if removed, False if not found
|
|
192
|
+
"""
|
|
193
|
+
if not macro.startswith(MACRO_PREFIX):
|
|
194
|
+
macro = MACRO_PREFIX + macro
|
|
195
|
+
|
|
196
|
+
if macro in self._macros:
|
|
197
|
+
del self._macros[macro]
|
|
198
|
+
Info("idlib", "unregistered", macro)
|
|
199
|
+
return True
|
|
200
|
+
return False
|
|
201
|
+
|
|
202
|
+
def resolve(self, identity: str) -> str:
|
|
203
|
+
"""
|
|
204
|
+
Resolve identity to XPath.
|
|
205
|
+
|
|
206
|
+
Supports:
|
|
207
|
+
- Plain xpath: "//div[@id='main']" -> returns as-is
|
|
208
|
+
- ID prefix: "@login" -> "//*[@id='login']"
|
|
209
|
+
- Macro: "$login_btn" -> resolves from library
|
|
210
|
+
- Inline register: "$btn: //button" -> registers and returns
|
|
211
|
+
- Parameterized: "$btn('ok')" -> applies format
|
|
212
|
+
|
|
213
|
+
Args:
|
|
214
|
+
identity: identity string
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
XPath expression
|
|
218
|
+
|
|
219
|
+
Raises:
|
|
220
|
+
KeyError: If macro not found
|
|
221
|
+
ValueError: If parameterized macro format is invalid
|
|
222
|
+
"""
|
|
223
|
+
_ident = identity.strip()
|
|
224
|
+
|
|
225
|
+
if not _ident:
|
|
226
|
+
return _ident
|
|
227
|
+
|
|
228
|
+
# Case 1: ID prefix @id -> xpath "//*[@id='id']"
|
|
229
|
+
if _ident.startswith(ID_PREFIX):
|
|
230
|
+
_id_value = _ident[1:].strip()
|
|
231
|
+
return f"//*[@id='{_id_value}']"
|
|
232
|
+
|
|
233
|
+
# Case 2: Macro prefix $
|
|
234
|
+
if _ident.startswith(MACRO_PREFIX):
|
|
235
|
+
return self._resolveMacro(_ident)
|
|
236
|
+
|
|
237
|
+
# Case 3: Plain xpath or other - return as-is
|
|
238
|
+
return _ident
|
|
239
|
+
|
|
240
|
+
def _resolveMacro(self, identity: str) -> str:
|
|
241
|
+
"""
|
|
242
|
+
Resolve macro expression.
|
|
243
|
+
|
|
244
|
+
Handles:
|
|
245
|
+
- $macro -> lookup in library
|
|
246
|
+
- $macro: xpath -> inline register
|
|
247
|
+
- $macro('arg') -> parameterized lookup
|
|
248
|
+
"""
|
|
249
|
+
# Check for inline definition: $macro: xpath
|
|
250
|
+
_colon_idx = identity.find(":")
|
|
251
|
+
if _colon_idx != -1:
|
|
252
|
+
# Make sure it's not part of a parameterized call like $macro('arg'): xpath
|
|
253
|
+
_paren_idx = identity.find("(")
|
|
254
|
+
if _paren_idx == -1 or _colon_idx < _paren_idx:
|
|
255
|
+
_key = re.sub(r"\s", "", identity[:_colon_idx])
|
|
256
|
+
if "(" in _key or ")" in _key:
|
|
257
|
+
raise KeyError(f"[IdentityError]: Cannot use '()' in def. Like: '$xxx():YYYY'.")
|
|
258
|
+
_value = identity[_colon_idx + 1:].strip()
|
|
259
|
+
self._library[_key] = IdentityRecord(xpath=_value)
|
|
260
|
+
Info("idlib", "inline registered", _key)
|
|
261
|
+
return _value
|
|
262
|
+
|
|
263
|
+
# Check for parameterized: $macro('arg1', "arg2")
|
|
264
|
+
_match = _PARAM_PATTERN.match(identity)
|
|
265
|
+
if _match:
|
|
266
|
+
_key = re.sub(r"\s", "", _match.group(1))
|
|
267
|
+
if not _key.startswith(MACRO_PREFIX):
|
|
268
|
+
_key = MACRO_PREFIX + _key
|
|
269
|
+
_params_str = _match.group(2)
|
|
270
|
+
# Extract string literals
|
|
271
|
+
_params = re.findall(_STRING_PATTERN, _params_str)
|
|
272
|
+
# Remove quotes from params
|
|
273
|
+
_params = [p[1:-1] for p in _params]
|
|
274
|
+
|
|
275
|
+
if _key not in self._macros:
|
|
276
|
+
raise KeyError(f"[IdentityMissing]: Do not have identity:{_key} in library.")
|
|
277
|
+
|
|
278
|
+
_xpath = self._macros[_key].xpath
|
|
279
|
+
if _params:
|
|
280
|
+
try:
|
|
281
|
+
_xpath = _xpath.format(*_params)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
raise ValueError(f"Format Failed: {e}. \n\traw:'{_xpath}'\n\treplaces:{_params}")
|
|
284
|
+
return _xpath
|
|
285
|
+
|
|
286
|
+
# Simple macro lookup
|
|
287
|
+
if identity not in self._macros:
|
|
288
|
+
raise KeyError(f"Macro '{identity}' not found")
|
|
289
|
+
|
|
290
|
+
return self._macros[identity].xpath
|
|
291
|
+
|
|
292
|
+
def get(self, macro: str) -> IdentityRecord | None:
|
|
293
|
+
"""
|
|
294
|
+
Get identity record by macro.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
macro: Macro name
|
|
298
|
+
|
|
299
|
+
Returns:
|
|
300
|
+
IdentityRecord or None
|
|
301
|
+
"""
|
|
302
|
+
if not macro.startswith(MACRO_PREFIX):
|
|
303
|
+
macro = MACRO_PREFIX + macro
|
|
304
|
+
return self._macros.get(macro)
|
|
305
|
+
|
|
306
|
+
def test(self, identity: str) -> bool:
|
|
307
|
+
"""
|
|
308
|
+
Test if identity is valid (macro exists or valid xpath).
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
identity: xpath or $macro to test
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
True if valid
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
self.resolve(identity)
|
|
318
|
+
return True
|
|
319
|
+
except (KeyError, ValueError):
|
|
320
|
+
return False
|
|
321
|
+
|
|
322
|
+
def list(self, tag: str | None = None) -> list[str]:
|
|
323
|
+
"""
|
|
324
|
+
List all registered macros.
|
|
325
|
+
|
|
326
|
+
Args:
|
|
327
|
+
tag: Filter by tag, None for all
|
|
328
|
+
|
|
329
|
+
Returns:
|
|
330
|
+
List of macro names
|
|
331
|
+
"""
|
|
332
|
+
if tag is None:
|
|
333
|
+
return list(self._macros.keys())
|
|
334
|
+
return [k for k, v in self._macros.items() if v.tag == tag]
|
|
335
|
+
|
|
336
|
+
def clear(self) -> Self:
|
|
337
|
+
"""
|
|
338
|
+
Clear all macros.
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
self for chaining
|
|
342
|
+
"""
|
|
343
|
+
self._macros.clear()
|
|
344
|
+
Info("idlib", "cleared")
|
|
345
|
+
return self
|
|
346
|
+
|
|
347
|
+
def batch(self, identities: dict[str, str], tag: str = "") -> Self:
|
|
348
|
+
"""
|
|
349
|
+
Batch register identities.
|
|
350
|
+
|
|
351
|
+
Args:
|
|
352
|
+
identities: Dict of macro -> xpath
|
|
353
|
+
tag: Default tag for all
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
self for chaining
|
|
357
|
+
"""
|
|
358
|
+
for macro, xpath in identities.items():
|
|
359
|
+
self.register(macro, xpath, tag=tag)
|
|
360
|
+
return self
|
|
361
|
+
|
|
362
|
+
def to_dict(self) -> dict[str, dict]:
|
|
363
|
+
"""Export all identities to dictionary."""
|
|
364
|
+
return {
|
|
365
|
+
k: {"xpath": v.xpath, "desc": v.desc, "tag": v.tag}
|
|
366
|
+
for k, v in self._macros.items()
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
def from_dict(self, data: dict[str, dict | str]) -> Self:
|
|
370
|
+
"""
|
|
371
|
+
Import identities from dictionary.
|
|
372
|
+
|
|
373
|
+
Args:
|
|
374
|
+
data: Dict of macro -> record or xpath string
|
|
375
|
+
|
|
376
|
+
Returns:
|
|
377
|
+
self for chaining
|
|
378
|
+
"""
|
|
379
|
+
for key, value in data.items():
|
|
380
|
+
if isinstance(value, str):
|
|
381
|
+
self.register(key, value)
|
|
382
|
+
else:
|
|
383
|
+
self.register(
|
|
384
|
+
key,
|
|
385
|
+
value.get("xpath", ""),
|
|
386
|
+
value.get("desc", ""),
|
|
387
|
+
value.get("tag", "")
|
|
388
|
+
)
|
|
389
|
+
return self
|
|
390
|
+
|
|
391
|
+
@property
|
|
392
|
+
def count(self) -> int:
|
|
393
|
+
"""Number of registered macros."""
|
|
394
|
+
return len(self._macros)
|
|
395
|
+
|
|
396
|
+
@property
|
|
397
|
+
def storage(self) -> str | None:
|
|
398
|
+
"""Current storage path."""
|
|
399
|
+
return self._storage_path
|
|
400
|
+
|
|
401
|
+
@property
|
|
402
|
+
def _library(self) -> dict[str, IdentityRecord]:
|
|
403
|
+
"""Direct access to macro library (for compatibility with inline register)."""
|
|
404
|
+
return self._macros
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# Global identity library instance
|
|
408
|
+
ID = IdentityLib()
|
|
409
|
+
|
|
410
|
+
|
|
411
|
+
def Identity(macro: str, xpath: str = "", desc: str = "") -> str:
|
|
412
|
+
"""
|
|
413
|
+
Register or resolve identity.
|
|
414
|
+
|
|
415
|
+
If xpath provided: register macro and return xpath
|
|
416
|
+
If xpath empty: resolve macro to xpath
|
|
417
|
+
|
|
418
|
+
Args:
|
|
419
|
+
macro: Macro name (auto-prefixed with $)
|
|
420
|
+
xpath: XPath expression (optional)
|
|
421
|
+
desc: Description (optional)
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
XPath expression
|
|
425
|
+
"""
|
|
426
|
+
if xpath:
|
|
427
|
+
ID.register(macro, xpath, desc)
|
|
428
|
+
return xpath
|
|
429
|
+
return ID.resolve(macro)
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def Register(macro: str, xpath: str, desc: str = "", tag: str = "") -> IdentityLib:
|
|
433
|
+
"""
|
|
434
|
+
Register identity macro.
|
|
435
|
+
|
|
436
|
+
Args:
|
|
437
|
+
macro: Macro name (auto-prefixed with $)
|
|
438
|
+
xpath: XPath expression
|
|
439
|
+
desc: Description
|
|
440
|
+
tag: Category tag
|
|
441
|
+
|
|
442
|
+
Returns:
|
|
443
|
+
IdentityLib instance for chaining
|
|
444
|
+
"""
|
|
445
|
+
return ID.register(macro, xpath, desc, tag)
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
def Resolve(identity: str) -> str:
|
|
449
|
+
"""
|
|
450
|
+
Resolve identity to XPath.
|
|
451
|
+
|
|
452
|
+
Supports:
|
|
453
|
+
- Plain xpath: "//div[@id='main']"
|
|
454
|
+
- ID prefix: "@login" -> "//*[@id='login']"
|
|
455
|
+
- Macro: "$login_btn" -> resolves from library
|
|
456
|
+
- Inline: "$btn: //button" -> registers and returns
|
|
457
|
+
- Parameterized: "$btn('ok')" -> applies format
|
|
458
|
+
|
|
459
|
+
Args:
|
|
460
|
+
identity: xpath, @id, $macro, etc.
|
|
461
|
+
|
|
462
|
+
Returns:
|
|
463
|
+
XPath expression
|
|
464
|
+
"""
|
|
465
|
+
return ID.resolve(identity)
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
def Batch(identities: dict[str, str], tag: str = "") -> IdentityLib:
|
|
469
|
+
"""
|
|
470
|
+
Batch register identities.
|
|
471
|
+
|
|
472
|
+
Args:
|
|
473
|
+
identities: Dict of macro -> xpath
|
|
474
|
+
tag: Default tag
|
|
475
|
+
|
|
476
|
+
Returns:
|
|
477
|
+
IdentityLib instance
|
|
478
|
+
"""
|
|
479
|
+
return ID.batch(identities, tag)
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
__all__ = [
|
|
483
|
+
"IdentityLib",
|
|
484
|
+
"IdentityRecord",
|
|
485
|
+
"ID",
|
|
486
|
+
"Identity",
|
|
487
|
+
"Register",
|
|
488
|
+
"Resolve",
|
|
489
|
+
"Batch",
|
|
490
|
+
"MACRO_PREFIX",
|
|
491
|
+
"ID_PREFIX",
|
|
492
|
+
]
|