python-playwright-helper 0.0.2__tar.gz → 0.1.7__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.
Files changed (23) hide show
  1. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/PKG-INFO +3 -3
  2. python_playwright_helper-0.1.7/playwright_helper/libs/base_po.py +113 -0
  3. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/executor.py +71 -21
  4. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/po_utils.py +0 -8
  5. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/type_utils.py +20 -2
  6. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/pyproject.toml +3 -3
  7. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/PKG-INFO +3 -3
  8. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/SOURCES.txt +1 -0
  9. python_playwright_helper-0.1.7/python_playwright_helper.egg-info/requires.txt +4 -0
  10. python_playwright_helper-0.0.2/python_playwright_helper.egg-info/requires.txt +0 -4
  11. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/LICENSE +0 -0
  12. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/README.md +0 -0
  13. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/__init__.py +0 -0
  14. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/__init__.py +0 -0
  15. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/libs/browser_pool.py +0 -0
  16. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/middlewares/__init__.py +0 -0
  17. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/middlewares/stealth.py +0 -0
  18. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/__init__.py +0 -0
  19. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/file_handle.py +0 -0
  20. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/playwright_helper/utils/log_utils.py +0 -0
  21. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/dependency_links.txt +0 -0
  22. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/python_playwright_helper.egg-info/top_level.txt +0 -0
  23. {python_playwright_helper-0.0.2 → python_playwright_helper-0.1.7}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_playwright_helper
3
- Version: 0.0.2
3
+ Version: 0.1.7
4
4
  Summary: playwright helper python package
5
5
  Author-email: ckf10000 <ckf10000@sina.com>
6
6
  License: Apache License
@@ -210,8 +210,8 @@ Project-URL: Issues, https://github.com/ckf10000/playwright-helper/issues
210
210
  Requires-Python: >=3.12
211
211
  Description-Content-Type: text/markdown
212
212
  License-File: LICENSE
213
- Requires-Dist: playwright>=1.56.0; python_version >= "3.12"
214
- Requires-Dist: playwright-stealth>=2.0.0; python_version >= "3.12"
213
+ Requires-Dist: playwright==1.56.0; python_version >= "3.12"
214
+ Requires-Dist: playwright-stealth==2.0.0; python_version >= "3.12"
215
215
  Dynamic: license-file
216
216
 
217
217
  # playwright-helper
@@ -0,0 +1,113 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ # ---------------------------------------------------------------------------------------------------------
4
+ # ProjectName: playwright-helper
5
+ # FileName: base_po.py
6
+ # Description: po对象基础类
7
+ # Author: ASUS
8
+ # CreateDate: 2025/12/13
9
+ # Copyright ©2011-2025. Hunan xxxxxxx Company limited. All rights reserved.
10
+ # ---------------------------------------------------------------------------------------------------------
11
+ """
12
+ import asyncio
13
+ from logging import Logger
14
+ from typing import List, Any, cast
15
+ from playwright.async_api import Page, Locator, TimeoutError as PlaywrightTimeoutError
16
+
17
+
18
+ class BasePo(object):
19
+ __page: Page
20
+
21
+ def __init__(self, page: Page, url: str):
22
+ self.url = url
23
+ self.__page = page
24
+
25
+ def get_page(self) -> Page:
26
+ return self.__page
27
+
28
+ def is_current_page(self) -> bool:
29
+ return self.iss_current_page(self.__page, self.url)
30
+
31
+ def get_url_domain(self) -> str:
32
+ if isinstance(self.__page, Page):
33
+ page_slice: List[str] = self.__page.url.split("/")
34
+ return f"{page_slice[0]}://{page_slice[2]}"
35
+ else:
36
+ raise AttributeError("PO对象中的page属性未被初始化")
37
+
38
+ @staticmethod
39
+ def iss_current_page(page: Page, url: str) -> bool:
40
+ if isinstance(page, Page):
41
+ page_url_prefix = page.url.split("?")[0]
42
+ url_prefix = url.split("?")[0]
43
+ if page_url_prefix.endswith(url_prefix):
44
+ return True
45
+ else:
46
+ return False
47
+ else:
48
+ return False
49
+
50
+ @staticmethod
51
+ async def exists(locator):
52
+ return await locator.count() > 0
53
+
54
+ @staticmethod
55
+ async def exists_one(locator):
56
+ return await locator.count() == 1
57
+
58
+ async def get_locator(self, selector: str, timeout: float = 3.0) -> Locator:
59
+ """
60
+ 获取页面元素locator
61
+ :param selector: 选择器表达式
62
+ :param timeout: 超时时间(秒)
63
+ :return: 元素对象
64
+ :return:
65
+ """
66
+ locator = self.__page.locator(selector)
67
+ try:
68
+ await locator.first.wait_for(state='visible', timeout=timeout * 1000)
69
+ return locator
70
+ except (PlaywrightTimeoutError,):
71
+ raise PlaywrightTimeoutError(f"元素 '{selector}' 未在 {timeout} 秒内找到")
72
+ except Exception as e:
73
+ raise RuntimeError(f"检查元素时发生错误: {str(e)}")
74
+
75
+ @staticmethod
76
+ async def get_sub_locator(locator: Locator, selector: str, timeout: float = 3.0) -> Locator:
77
+ """
78
+ 获取页面locator的子locator
79
+ :param locator: 页面Locator对象
80
+ :param selector: 选择器表达式
81
+ :param timeout: 超时时间(秒)
82
+ :return: 元素对象
83
+ :return:
84
+ """
85
+ locator_inner = locator.locator(selector)
86
+ try:
87
+ await locator_inner.first.wait_for(state='visible', timeout=timeout * 1000)
88
+ return locator_inner
89
+ except (PlaywrightTimeoutError,):
90
+ raise PlaywrightTimeoutError(f"元素 '{selector}' 未在 {timeout} 秒内找到")
91
+ except Exception as e:
92
+ raise RuntimeError(f"检查元素时发生错误: {str(e)}")
93
+
94
+ @classmethod
95
+ async def handle_po_cookie_tip(cls, page: Any, logger: Logger, timeout: float = 3.0, selectors: List[str] = None) -> None:
96
+ selectors_inner: List[str] = [
97
+ '//div[@id="isReadedCookie"]/button',
98
+ '//button[@id="continue-btn"]/span[normalize-space(text())="同意"]'
99
+ ]
100
+ if selectors:
101
+ selectors_inner.extend(selectors)
102
+ for selector in selectors_inner:
103
+ try:
104
+ page_inner = cast(cls, page)
105
+ cookie: Locator = await cls.get_locator(self=page_inner, selector=selector, timeout=timeout)
106
+ logger.info(
107
+ f'找到页面中存在cookie提示:[本网站使用cookie,用于在您的电脑中储存信息。这些cookie可以使网站正常运行,以及帮助我们改进用户体验。使用本网站,即表示您接受放置这些cookie。]')
108
+ await cookie.click(button="left")
109
+ logger.info("【同意】按钮点击完成")
110
+ await asyncio.sleep(1)
111
+ return
112
+ except (Exception,):
113
+ pass
@@ -15,11 +15,13 @@ import uuid
15
15
  import asyncio
16
16
  from logging import Logger
17
17
  from playwright.async_api import async_playwright
18
+ from playwright_helper.utils.type_utils import RunResult
18
19
  from playwright_helper.utils.log_utils import logger as log
19
20
  from playwright_helper.libs.browser_pool import BrowserPool
20
21
  from playwright_helper.utils.file_handle import get_caller_dir
21
- from playwright.async_api import Page, Browser, BrowserContext
22
22
  from typing import Any, List, Optional, cast, Callable, Literal
23
+ from playwright.async_api import Page, Browser, BrowserContext, TimeoutError as PlaywrightTimeoutError, \
24
+ Error as PlaywrightError
23
25
 
24
26
 
25
27
  class PlaywrightBrowserExecutor:
@@ -61,7 +63,7 @@ class PlaywrightBrowserExecutor:
61
63
  async def _safe_screenshot(self, page: Page, name: str = None):
62
64
  try:
63
65
  os.makedirs(self.screenshot_dir, exist_ok=True)
64
- if name is None:
66
+ if name is None or "unknown" in name:
65
67
  name = f"error_{int(time.time())}"
66
68
  path = os.path.join(self.screenshot_dir, f"{name}.png")
67
69
  await page.screenshot(path=path)
@@ -121,12 +123,23 @@ class PlaywrightBrowserExecutor:
121
123
  return context
122
124
 
123
125
  async def _cleanup_context(self, context: BrowserContext):
124
- await context.close()
126
+ if getattr(context, "_closed", False):
127
+ return
128
+
129
+ context._closed = True # type: ignore
130
+
131
+ try:
132
+ await context.close()
133
+ except Exception as e:
134
+ self.logger.debug(f"[Cleanup] ignore close error: {e}")
125
135
 
126
136
  browser = getattr(context, "_browser", None)
127
137
  if browser:
128
- self.logger.debug("[Executor] Release browser to pool")
129
- await self.browser_pool.release(browser)
138
+ try:
139
+ self.logger.debug("[Executor] Release browser to pool")
140
+ await self.browser_pool.release(browser)
141
+ except Exception as e:
142
+ self.logger.error(f"[Cleanup] ignore release error: {e}")
130
143
 
131
144
  async def _run_callback_chain(self, *, callback: Callable, page: Page, context: BrowserContext, **kwargs) -> Any:
132
145
  # Run middlewares before callback
@@ -136,38 +149,75 @@ class PlaywrightBrowserExecutor:
136
149
  # Main callback
137
150
  return await callback(page=page, logger=self.logger, context=context, **kwargs)
138
151
 
139
- async def run(self, *, callback: Callable, **kwargs: Any) -> Any:
152
+ async def run(self, *, callback: Callable, **kwargs: Any) -> RunResult:
140
153
  attempt = 0
154
+ last_error: Optional[Exception] = None
155
+ task_id: Optional[str] = "unknown"
156
+ result: Any = None
141
157
 
142
158
  while attempt <= self.retries:
143
159
  page = None
144
160
  context: BrowserContext = cast(BrowserContext, None)
145
161
  try:
146
162
  context = await self._create_context()
163
+ task_id = getattr(context, "_task_id", "unknown")
147
164
  page = await context.new_page()
148
165
 
149
166
  result = await self._run_callback_chain(
150
167
  callback=callback, page=page, context=context, **kwargs
151
168
  )
152
- return result
153
-
154
- except Exception as e:
155
- task_id = getattr(context, "_task_id", "unknown")
156
- self.logger.error(f"[Task<{task_id}> Attempt {attempt} Failed] {e}")
169
+ self.logger.info(f"[Task<{task_id}> Success]")
170
+ return RunResult(
171
+ success=True,
172
+ attempts=attempt + 1,
173
+ task_id=task_id,
174
+ error=last_error,
175
+ result=result
176
+ )
177
+ except (asyncio.CancelledError, KeyboardInterrupt):
178
+ # ⚠️ 不要当成错误
179
+ self.logger.warning(f"[Task<{task_id}> Cancelled]")
180
+
181
+ # 清理资源
182
+ if context and self.record_trace:
183
+ os.makedirs(self.trace_dir, exist_ok=True)
184
+ trace_path = os.path.join(self.trace_dir, f"{task_id}.zip")
185
+ await context.tracing.stop(path=trace_path)
186
+ self.logger.error(f"[Trace Saved] {trace_path}")
187
+
188
+ # ❗关键:不要 raise
189
+ return RunResult(
190
+ success=False,
191
+ attempts=attempt + 1,
192
+ error=asyncio.CancelledError(),
193
+ task_id=task_id,
194
+ result=result
195
+ )
196
+ except (PlaywrightTimeoutError, PlaywrightError, Exception) as e:
197
+ last_error = e
198
+ self.logger.error(f"[Task<{task_id}> Attempt {attempt + 1} Failed] {e}")
157
199
  if page:
158
- await self._safe_screenshot(page, task_id)
200
+ await self._safe_screenshot(page=page, name=task_id)
159
201
 
160
- if self.record_trace:
161
- os.makedirs(self.trace_dir, exist_ok=True)
162
- trace_path = os.path.join(self.trace_dir, f"{task_id}.zip")
163
- await context.tracing.stop(path=trace_path)
164
- self.logger.error(f"[Trace Saved] {trace_path}")
202
+ if context and self.record_trace:
203
+ os.makedirs(self.trace_dir, exist_ok=True)
204
+ trace_path = os.path.join(self.trace_dir, f"{task_id}.zip")
205
+ await context.tracing.stop(path=trace_path)
206
+ self.logger.error(f"[Trace Saved] {trace_path}")
165
207
 
166
208
  attempt += 1
167
- if attempt > self.retries:
168
- self.logger.error("[Final Failure]")
169
- raise e
170
- await asyncio.sleep(1)
209
+ if attempt <= self.retries:
210
+ await asyncio.sleep(1)
171
211
  finally:
172
212
  if context:
173
213
  await self._cleanup_context(context)
214
+ # 所有重试结束,仍然失败
215
+ self.logger.error(f"[Task<{task_id}> Final Failure]")
216
+
217
+ return RunResult(
218
+ success=False,
219
+ attempts=attempt,
220
+ error=last_error,
221
+ task_id=task_id,
222
+ result=result
223
+ )
@@ -15,14 +15,6 @@ from playwright.async_api import Page, Locator
15
15
  MouseButton = Literal["left", "middle", "right"]
16
16
 
17
17
 
18
- async def exists(locator):
19
- return await locator.count() > 0
20
-
21
-
22
- async def exists_one(locator):
23
- return await locator.count() == 1
24
-
25
-
26
18
  async def on_click_locator(locator: Locator, button: MouseButton = "left") -> bool:
27
19
  try:
28
20
  await locator.click(button=button)
@@ -10,7 +10,8 @@
10
10
  # ---------------------------------------------------------------------------------------------------------
11
11
  """
12
12
  import re
13
- from typing import Any, Dict, Tuple, Union
13
+ from dataclasses import dataclass
14
+ from typing import Any, Dict, Tuple, Union, Optional
14
15
 
15
16
 
16
17
  def safe_convert_advanced(value, return_type='auto') -> Any:
@@ -85,6 +86,7 @@ def safe_convert_advanced(value, return_type='auto') -> Any:
85
86
  # 所有转换都失败,返回原值
86
87
  return value
87
88
 
89
+
88
90
  def convert_order_amount_text(amount_text: str) -> Tuple[bool, Union[Dict[str, Any], str]]:
89
91
  """
90
92
  将页面中的金额文案,解析成字典数据
@@ -108,4 +110,20 @@ def convert_order_amount_text(amount_text: str) -> Tuple[bool, Union[Dict[str, A
108
110
  amount = safe_convert_advanced(value=amount_str.replace(",", ""))
109
111
  else:
110
112
  return False, f"订单金额文案[{amount_text}]解析金额信息有异常"
111
- return True, dict(currency=currency, amount=amount)
113
+ return True, dict(currency=currency, amount=amount)
114
+
115
+
116
+ @dataclass
117
+ class RunResult:
118
+ """
119
+ success:是否成功
120
+ attempts:实际尝试次数
121
+ error:最终失败的异常(有就带,没有就 None)
122
+ task_id:方便框架层日志关联
123
+ result: 执行结果
124
+ """
125
+ success: bool
126
+ attempts: int
127
+ error: Optional[Exception] = None
128
+ task_id: Optional[str] = None
129
+ result: Optional[Any] = None
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "python_playwright_helper" # pip包名(不要大写)
7
- version = "0.0.2" # 版本号
7
+ version = "0.1.7" # 版本号
8
8
  description = "playwright helper python package"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.12" # Python 版本要求
@@ -16,8 +16,8 @@ authors = [
16
16
 
17
17
  # 运行时依赖
18
18
  dependencies = [
19
- "playwright>=1.56.0; python_version >= '3.12'",
20
- "playwright-stealth>=2.0.0; python_version >= '3.12'",
19
+ "playwright==1.56.0; python_version >= '3.12'",
20
+ "playwright-stealth==2.0.0; python_version >= '3.12'",
21
21
  ]
22
22
 
23
23
  [project.urls]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python_playwright_helper
3
- Version: 0.0.2
3
+ Version: 0.1.7
4
4
  Summary: playwright helper python package
5
5
  Author-email: ckf10000 <ckf10000@sina.com>
6
6
  License: Apache License
@@ -210,8 +210,8 @@ Project-URL: Issues, https://github.com/ckf10000/playwright-helper/issues
210
210
  Requires-Python: >=3.12
211
211
  Description-Content-Type: text/markdown
212
212
  License-File: LICENSE
213
- Requires-Dist: playwright>=1.56.0; python_version >= "3.12"
214
- Requires-Dist: playwright-stealth>=2.0.0; python_version >= "3.12"
213
+ Requires-Dist: playwright==1.56.0; python_version >= "3.12"
214
+ Requires-Dist: playwright-stealth==2.0.0; python_version >= "3.12"
215
215
  Dynamic: license-file
216
216
 
217
217
  # playwright-helper
@@ -3,6 +3,7 @@ README.md
3
3
  pyproject.toml
4
4
  playwright_helper/__init__.py
5
5
  playwright_helper/libs/__init__.py
6
+ playwright_helper/libs/base_po.py
6
7
  playwright_helper/libs/browser_pool.py
7
8
  playwright_helper/libs/executor.py
8
9
  playwright_helper/middlewares/__init__.py
@@ -0,0 +1,4 @@
1
+
2
+ [:python_version >= "3.12"]
3
+ playwright==1.56.0
4
+ playwright-stealth==2.0.0
@@ -1,4 +0,0 @@
1
-
2
- [:python_version >= "3.12"]
3
- playwright>=1.56.0
4
- playwright-stealth>=2.0.0