kdtest-pw 2.0.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.
Files changed (57) hide show
  1. kdtest_pw/__init__.py +50 -0
  2. kdtest_pw/action/__init__.py +7 -0
  3. kdtest_pw/action/base_keyword.py +292 -0
  4. kdtest_pw/action/element_plus/__init__.py +23 -0
  5. kdtest_pw/action/element_plus/el_cascader.py +263 -0
  6. kdtest_pw/action/element_plus/el_datepicker.py +324 -0
  7. kdtest_pw/action/element_plus/el_dialog.py +317 -0
  8. kdtest_pw/action/element_plus/el_form.py +443 -0
  9. kdtest_pw/action/element_plus/el_menu.py +456 -0
  10. kdtest_pw/action/element_plus/el_select.py +268 -0
  11. kdtest_pw/action/element_plus/el_table.py +442 -0
  12. kdtest_pw/action/element_plus/el_tree.py +364 -0
  13. kdtest_pw/action/element_plus/el_upload.py +313 -0
  14. kdtest_pw/action/key_retrieval.py +311 -0
  15. kdtest_pw/action/page_action.py +1129 -0
  16. kdtest_pw/api/__init__.py +6 -0
  17. kdtest_pw/api/api_keyword.py +251 -0
  18. kdtest_pw/api/request_handler.py +232 -0
  19. kdtest_pw/cases/__init__.py +6 -0
  20. kdtest_pw/cases/case_collector.py +182 -0
  21. kdtest_pw/cases/case_executor.py +359 -0
  22. kdtest_pw/cases/read/__init__.py +6 -0
  23. kdtest_pw/cases/read/cell_handler.py +305 -0
  24. kdtest_pw/cases/read/excel_reader.py +223 -0
  25. kdtest_pw/cli/__init__.py +5 -0
  26. kdtest_pw/cli/run.py +318 -0
  27. kdtest_pw/common.py +106 -0
  28. kdtest_pw/core/__init__.py +7 -0
  29. kdtest_pw/core/browser_manager.py +196 -0
  30. kdtest_pw/core/config_loader.py +235 -0
  31. kdtest_pw/core/page_context.py +228 -0
  32. kdtest_pw/data/__init__.py +5 -0
  33. kdtest_pw/data/init_data.py +105 -0
  34. kdtest_pw/data/static/elementData.yaml +59 -0
  35. kdtest_pw/data/static/parameters.json +24 -0
  36. kdtest_pw/plugins/__init__.py +6 -0
  37. kdtest_pw/plugins/element_plus_plugin/__init__.py +5 -0
  38. kdtest_pw/plugins/element_plus_plugin/elementData/elementData.yaml +144 -0
  39. kdtest_pw/plugins/element_plus_plugin/element_plus_plugin.py +237 -0
  40. kdtest_pw/plugins/element_plus_plugin/my.ini +23 -0
  41. kdtest_pw/plugins/plugin_base.py +180 -0
  42. kdtest_pw/plugins/plugin_loader.py +260 -0
  43. kdtest_pw/product.py +5 -0
  44. kdtest_pw/reference.py +99 -0
  45. kdtest_pw/utils/__init__.py +13 -0
  46. kdtest_pw/utils/built_in_function.py +376 -0
  47. kdtest_pw/utils/decorator.py +211 -0
  48. kdtest_pw/utils/log/__init__.py +6 -0
  49. kdtest_pw/utils/log/html_report.py +336 -0
  50. kdtest_pw/utils/log/logger.py +123 -0
  51. kdtest_pw/utils/public_script.py +366 -0
  52. kdtest_pw-2.0.0.dist-info/METADATA +169 -0
  53. kdtest_pw-2.0.0.dist-info/RECORD +57 -0
  54. kdtest_pw-2.0.0.dist-info/WHEEL +5 -0
  55. kdtest_pw-2.0.0.dist-info/entry_points.txt +2 -0
  56. kdtest_pw-2.0.0.dist-info/licenses/LICENSE +21 -0
  57. kdtest_pw-2.0.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,359 @@
1
+ """用例执行引擎"""
2
+
3
+ import time
4
+ import traceback
5
+ from typing import Dict, Any, Optional, List, Callable
6
+ from datetime import datetime
7
+ from pathlib import Path
8
+
9
+ from .read.excel_reader import TestCase, TestStep
10
+ from .read.cell_handler import CellHandler
11
+ from ..action.key_retrieval import KeyRetrieval
12
+ from ..reference import (
13
+ GSTORE, PRIVATEDATA, CASESDATA, INFO,
14
+ set_element_value, clear_element_values
15
+ )
16
+
17
+
18
+ class StepResult:
19
+ """步骤执行结果"""
20
+
21
+ def __init__(self, step: TestStep):
22
+ self.step = step
23
+ self.status: str = 'pending' # pending, passed, failed, skipped
24
+ self.start_time: Optional[datetime] = None
25
+ self.end_time: Optional[datetime] = None
26
+ self.duration: float = 0
27
+ self.error: Optional[str] = None
28
+ self.screenshot: Optional[str] = None
29
+
30
+ @property
31
+ def is_passed(self) -> bool:
32
+ return self.status == 'passed'
33
+
34
+ @property
35
+ def is_failed(self) -> bool:
36
+ return self.status == 'failed'
37
+
38
+
39
+ class CaseResult:
40
+ """用例执行结果"""
41
+
42
+ def __init__(self, case: TestCase):
43
+ self.case = case
44
+ self.status: str = 'pending' # pending, passed, failed, skipped
45
+ self.start_time: Optional[datetime] = None
46
+ self.end_time: Optional[datetime] = None
47
+ self.duration: float = 0
48
+ self.step_results: List[StepResult] = []
49
+ self.error: Optional[str] = None
50
+
51
+ @property
52
+ def is_passed(self) -> bool:
53
+ return self.status == 'passed'
54
+
55
+ @property
56
+ def is_failed(self) -> bool:
57
+ return self.status == 'failed'
58
+
59
+ @property
60
+ def passed_steps(self) -> int:
61
+ return sum(1 for s in self.step_results if s.is_passed)
62
+
63
+ @property
64
+ def failed_steps(self) -> int:
65
+ return sum(1 for s in self.step_results if s.is_failed)
66
+
67
+
68
+ class CaseExecutor:
69
+ """用例执行引擎
70
+
71
+ 执行测试用例,管理执行流程和结果收集。
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ key_retrieval: KeyRetrieval = None,
77
+ cell_handler: CellHandler = None,
78
+ retry_count: int = 0,
79
+ screenshot_on_fail: bool = True,
80
+ stop_on_fail: bool = False
81
+ ):
82
+ """初始化执行引擎
83
+
84
+ Args:
85
+ key_retrieval: 关键字分发器
86
+ cell_handler: 单元格处理器
87
+ retry_count: 失败重试次数
88
+ screenshot_on_fail: 失败时是否截图
89
+ stop_on_fail: 失败时是否停止
90
+ """
91
+ self._key_retrieval = key_retrieval or KeyRetrieval()
92
+ self._cell_handler = cell_handler or CellHandler()
93
+ self._retry_count = retry_count
94
+ self._screenshot_on_fail = screenshot_on_fail
95
+ self._stop_on_fail = stop_on_fail
96
+
97
+ self._results: List[CaseResult] = []
98
+ self._current_case: Optional[TestCase] = None
99
+ self._current_result: Optional[CaseResult] = None
100
+
101
+ # 回调
102
+ self._on_case_start: Optional[Callable] = None
103
+ self._on_case_end: Optional[Callable] = None
104
+ self._on_step_start: Optional[Callable] = None
105
+ self._on_step_end: Optional[Callable] = None
106
+
107
+ def set_callbacks(
108
+ self,
109
+ on_case_start: Callable = None,
110
+ on_case_end: Callable = None,
111
+ on_step_start: Callable = None,
112
+ on_step_end: Callable = None
113
+ ) -> None:
114
+ """设置回调函数
115
+
116
+ Args:
117
+ on_case_start: 用例开始回调
118
+ on_case_end: 用例结束回调
119
+ on_step_start: 步骤开始回调
120
+ on_step_end: 步骤结束回调
121
+ """
122
+ self._on_case_start = on_case_start
123
+ self._on_case_end = on_case_end
124
+ self._on_step_start = on_step_start
125
+ self._on_step_end = on_step_end
126
+
127
+ def execute_cases(self, cases: List[TestCase]) -> List[CaseResult]:
128
+ """执行多个用例
129
+
130
+ Args:
131
+ cases: 用例列表
132
+
133
+ Returns:
134
+ List[CaseResult]: 执行结果列表
135
+ """
136
+ self._results.clear()
137
+ CASESDATA['total'] = len(cases)
138
+ CASESDATA['passed'] = 0
139
+ CASESDATA['failed'] = 0
140
+ CASESDATA['skipped'] = 0
141
+
142
+ INFO(f"开始执行 {len(cases)} 个用例")
143
+
144
+ for case in cases:
145
+ result = self.execute_case(case)
146
+ self._results.append(result)
147
+
148
+ if result.is_passed:
149
+ CASESDATA['passed'] += 1
150
+ elif result.is_failed:
151
+ CASESDATA['failed'] += 1
152
+ if self._stop_on_fail:
153
+ INFO("用例失败,停止执行", "WARNING")
154
+ break
155
+ else:
156
+ CASESDATA['skipped'] += 1
157
+
158
+ INFO(f"执行完成: 通过 {CASESDATA['passed']}, 失败 {CASESDATA['failed']}, 跳过 {CASESDATA['skipped']}")
159
+ return self._results
160
+
161
+ def execute_case(self, case: TestCase) -> CaseResult:
162
+ """执行单个用例
163
+
164
+ Args:
165
+ case: 测试用例
166
+
167
+ Returns:
168
+ CaseResult: 执行结果
169
+ """
170
+ self._current_case = case
171
+ self._current_result = CaseResult(case)
172
+ PRIVATEDATA['CURRENT_CASE'] = case.name
173
+
174
+ INFO(f"执行用例: [{case.id}] {case.name}")
175
+
176
+ # 回调
177
+ if self._on_case_start:
178
+ self._on_case_start(case)
179
+
180
+ self._current_result.start_time = datetime.now()
181
+
182
+ # 清除上一个用例的缓存
183
+ clear_element_values()
184
+
185
+ try:
186
+ # 执行所有步骤
187
+ for step in case.steps:
188
+ step_result = self._execute_step_with_retry(step)
189
+ self._current_result.step_results.append(step_result)
190
+
191
+ if step_result.is_failed and self._stop_on_fail:
192
+ break
193
+
194
+ # 判断用例结果
195
+ if any(s.is_failed for s in self._current_result.step_results):
196
+ self._current_result.status = 'failed'
197
+ else:
198
+ self._current_result.status = 'passed'
199
+
200
+ except Exception as e:
201
+ self._current_result.status = 'failed'
202
+ self._current_result.error = str(e)
203
+ INFO(f"用例执行异常: {e}", "ERROR")
204
+
205
+ self._current_result.end_time = datetime.now()
206
+ self._current_result.duration = (
207
+ self._current_result.end_time - self._current_result.start_time
208
+ ).total_seconds()
209
+
210
+ status_text = '通过' if self._current_result.is_passed else '失败'
211
+ INFO(f"用例完成: [{case.id}] {case.name} - {status_text} ({self._current_result.duration:.2f}s)")
212
+
213
+ # 回调
214
+ if self._on_case_end:
215
+ self._on_case_end(case, self._current_result)
216
+
217
+ return self._current_result
218
+
219
+ def _execute_step_with_retry(self, step: TestStep) -> StepResult:
220
+ """执行步骤(带重试)
221
+
222
+ Args:
223
+ step: 测试步骤
224
+
225
+ Returns:
226
+ StepResult: 步骤结果
227
+ """
228
+ result = None
229
+ retry = 0
230
+
231
+ while retry <= self._retry_count:
232
+ result = self._execute_step(step)
233
+
234
+ if result.is_passed:
235
+ break
236
+
237
+ if retry < self._retry_count:
238
+ INFO(f"步骤失败,第 {retry + 1} 次重试...", "WARNING")
239
+ retry += 1
240
+ else:
241
+ break
242
+
243
+ return result
244
+
245
+ def _execute_step(self, step: TestStep) -> StepResult:
246
+ """执行单个步骤
247
+
248
+ Args:
249
+ step: 测试步骤
250
+
251
+ Returns:
252
+ StepResult: 步骤结果
253
+ """
254
+ result = StepResult(step)
255
+
256
+ # 回调
257
+ if self._on_step_start:
258
+ self._on_step_start(step)
259
+
260
+ result.start_time = datetime.now()
261
+
262
+ try:
263
+ # 处理定位信息
264
+ locator_info = []
265
+ for loc in step.locator:
266
+ processed = self._cell_handler.process(loc)
267
+ if processed:
268
+ locator_info.append(processed)
269
+
270
+ # 处理操作值
271
+ value = self._cell_handler.process(step.value) if step.value else None
272
+
273
+ # 执行关键字
274
+ INFO(f" 步骤: {step.description or step.keyword}")
275
+ exec_result = self._key_retrieval.execute(
276
+ keyword=step.keyword,
277
+ locator_info=locator_info,
278
+ value=value
279
+ )
280
+
281
+ # 处理断言关键字返回值
282
+ if step.keyword.endswith('_assert') or step.keyword.startswith('assert'):
283
+ if exec_result is False:
284
+ raise AssertionError(f"断言失败: {step.keyword}")
285
+
286
+ result.status = 'passed'
287
+
288
+ except Exception as e:
289
+ result.status = 'failed'
290
+ result.error = str(e)
291
+ INFO(f" 步骤失败: {e}", "ERROR")
292
+
293
+ # 失败截图
294
+ if self._screenshot_on_fail:
295
+ result.screenshot = self._take_screenshot(step)
296
+
297
+ result.end_time = datetime.now()
298
+ result.duration = (result.end_time - result.start_time).total_seconds()
299
+
300
+ # 回调
301
+ if self._on_step_end:
302
+ self._on_step_end(step, result)
303
+
304
+ return result
305
+
306
+ def _take_screenshot(self, step: TestStep) -> Optional[str]:
307
+ """失败截图
308
+
309
+ Args:
310
+ step: 测试步骤
311
+
312
+ Returns:
313
+ str: 截图路径
314
+ """
315
+ try:
316
+ page = GSTORE.get('page')
317
+ if not page:
318
+ return None
319
+
320
+ # 生成截图文件名
321
+ timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
322
+ case_name = self._current_case.name if self._current_case else 'unknown'
323
+ # 移除非法字符
324
+ case_name = ''.join(c for c in case_name if c.isalnum() or c in ('_', '-'))
325
+
326
+ screenshot_dir = Path('result/screenshot')
327
+ screenshot_dir.mkdir(parents=True, exist_ok=True)
328
+
329
+ screenshot_path = screenshot_dir / f"{case_name}_{step.row}_{timestamp}.png"
330
+ page.screenshot(path=str(screenshot_path))
331
+
332
+ INFO(f" 截图已保存: {screenshot_path}")
333
+ return str(screenshot_path)
334
+
335
+ except Exception as e:
336
+ INFO(f" 截图失败: {e}", "WARNING")
337
+ return None
338
+
339
+ @property
340
+ def results(self) -> List[CaseResult]:
341
+ """获取所有执行结果"""
342
+ return self._results.copy()
343
+
344
+ @property
345
+ def summary(self) -> Dict[str, Any]:
346
+ """获取执行摘要"""
347
+ total = len(self._results)
348
+ passed = sum(1 for r in self._results if r.is_passed)
349
+ failed = sum(1 for r in self._results if r.is_failed)
350
+ duration = sum(r.duration for r in self._results)
351
+
352
+ return {
353
+ 'total': total,
354
+ 'passed': passed,
355
+ 'failed': failed,
356
+ 'skipped': total - passed - failed,
357
+ 'pass_rate': (passed / total * 100) if total > 0 else 0,
358
+ 'duration': duration,
359
+ }
@@ -0,0 +1,6 @@
1
+ """Excel 读取模块"""
2
+
3
+ from .excel_reader import ExcelReader
4
+ from .cell_handler import CellHandler
5
+
6
+ __all__ = ['ExcelReader', 'CellHandler']
@@ -0,0 +1,305 @@
1
+ """单元格值处理器"""
2
+
3
+ import re
4
+ from typing import Any, Optional, Dict
5
+ from datetime import datetime, timedelta
6
+
7
+ from ...reference import PRIVATEDATA, GSDSTORE, get_element_value, INFO
8
+
9
+
10
+ class CellHandler:
11
+ """单元格值处理器
12
+
13
+ 处理 Excel 单元格中的特殊语法,如变量引用、内置函数等。
14
+ """
15
+
16
+ # 变量引用模式: ${varName} 或 ${module.varName}
17
+ VAR_PATTERN = re.compile(r'\$\{([^}]+)\}')
18
+
19
+ # 内置函数模式: @func(args) 或 @func()
20
+ FUNC_PATTERN = re.compile(r'@(\w+)\(([^)]*)\)')
21
+
22
+ def __init__(self, custom_params: Dict[str, Any] = None):
23
+ """初始化处理器
24
+
25
+ Args:
26
+ custom_params: 自定义参数字典
27
+ """
28
+ self._custom_params = custom_params or {}
29
+
30
+ # 注册内置函数
31
+ self._functions = {
32
+ 'today': self._func_today,
33
+ 'now': self._func_now,
34
+ 'date': self._func_date,
35
+ 'random': self._func_random,
36
+ 'random_str': self._func_random_str,
37
+ 'random_phone': self._func_random_phone,
38
+ 'random_email': self._func_random_email,
39
+ 'uuid': self._func_uuid,
40
+ 'timestamp': self._func_timestamp,
41
+ 'env': self._func_env,
42
+ }
43
+
44
+ def process(self, value: Any) -> Any:
45
+ """处理单元格值
46
+
47
+ Args:
48
+ value: 原始值
49
+
50
+ Returns:
51
+ 处理后的值
52
+ """
53
+ if value is None:
54
+ return None
55
+
56
+ value_str = str(value)
57
+
58
+ # 处理变量引用
59
+ value_str = self._process_variables(value_str)
60
+
61
+ # 处理内置函数
62
+ value_str = self._process_functions(value_str)
63
+
64
+ return value_str
65
+
66
+ def _process_variables(self, value: str) -> str:
67
+ """处理变量引用
68
+
69
+ 支持格式:
70
+ - ${varName} - 从元素值缓存获取
71
+ - ${self.param} - 从自定义参数获取
72
+ - ${env.name} - 从环境变量获取
73
+
74
+ Args:
75
+ value: 原始值
76
+
77
+ Returns:
78
+ 处理后的值
79
+ """
80
+ def replace_var(match):
81
+ var_path = match.group(1)
82
+
83
+ # 检查是否有命名空间
84
+ if '.' in var_path:
85
+ namespace, var_name = var_path.split('.', 1)
86
+
87
+ if namespace == 'self':
88
+ # 自定义参数
89
+ return str(self._custom_params.get(var_name, match.group(0)))
90
+ elif namespace == 'env':
91
+ # 环境变量
92
+ import os
93
+ return os.environ.get(var_name, match.group(0))
94
+ elif namespace == 'ele':
95
+ # 元素值缓存
96
+ return str(get_element_value(var_name, match.group(0)))
97
+
98
+ # 默认从元素值缓存获取
99
+ cached = get_element_value(var_path)
100
+ if cached is not None:
101
+ return str(cached)
102
+
103
+ # 从自定义参数获取
104
+ if var_path in self._custom_params:
105
+ return str(self._custom_params[var_path])
106
+
107
+ # 返回原始匹配
108
+ return match.group(0)
109
+
110
+ return self.VAR_PATTERN.sub(replace_var, value)
111
+
112
+ def _process_functions(self, value: str) -> str:
113
+ """处理内置函数
114
+
115
+ 支持格式:
116
+ - @today() - 今天日期
117
+ - @now() - 当前时间
118
+ - @random(min, max) - 随机数
119
+ - @uuid() - UUID
120
+
121
+ Args:
122
+ value: 原始值
123
+
124
+ Returns:
125
+ 处理后的值
126
+ """
127
+ def replace_func(match):
128
+ func_name = match.group(1)
129
+ args_str = match.group(2).strip()
130
+
131
+ # 解析参数
132
+ args = []
133
+ if args_str:
134
+ args = [a.strip().strip('"\'') for a in args_str.split(',')]
135
+
136
+ # 调用函数
137
+ if func_name in self._functions:
138
+ try:
139
+ return str(self._functions[func_name](*args))
140
+ except Exception as e:
141
+ INFO(f"函数执行失败: @{func_name}({args_str}) - {e}", "WARNING")
142
+ return match.group(0)
143
+
144
+ return match.group(0)
145
+
146
+ return self.FUNC_PATTERN.sub(replace_func, value)
147
+
148
+ # ==================== 内置函数 ====================
149
+
150
+ def _func_today(self, fmt: str = '%Y-%m-%d', offset: str = '0') -> str:
151
+ """获取今天日期
152
+
153
+ Args:
154
+ fmt: 日期格式
155
+ offset: 天数偏移
156
+
157
+ Returns:
158
+ str: 格式化的日期
159
+ """
160
+ date = datetime.now() + timedelta(days=int(offset))
161
+ return date.strftime(fmt)
162
+
163
+ def _func_now(self, fmt: str = '%Y-%m-%d %H:%M:%S') -> str:
164
+ """获取当前时间
165
+
166
+ Args:
167
+ fmt: 时间格式
168
+
169
+ Returns:
170
+ str: 格式化的时间
171
+ """
172
+ return datetime.now().strftime(fmt)
173
+
174
+ def _func_date(self, fmt: str = '%Y-%m-%d', offset: str = '0', unit: str = 'days') -> str:
175
+ """获取日期
176
+
177
+ Args:
178
+ fmt: 日期格式
179
+ offset: 偏移量
180
+ unit: 单位 (days, hours, minutes, seconds)
181
+
182
+ Returns:
183
+ str: 格式化的日期
184
+ """
185
+ offset_int = int(offset)
186
+ kwargs = {unit: offset_int}
187
+ date = datetime.now() + timedelta(**kwargs)
188
+ return date.strftime(fmt)
189
+
190
+ def _func_random(self, min_val: str = '0', max_val: str = '100') -> int:
191
+ """生成随机整数
192
+
193
+ Args:
194
+ min_val: 最小值
195
+ max_val: 最大值
196
+
197
+ Returns:
198
+ int: 随机整数
199
+ """
200
+ import random
201
+ return random.randint(int(min_val), int(max_val))
202
+
203
+ def _func_random_str(self, length: str = '8', chars: str = 'alphanumeric') -> str:
204
+ """生成随机字符串
205
+
206
+ Args:
207
+ length: 长度
208
+ chars: 字符类型 (alpha, numeric, alphanumeric)
209
+
210
+ Returns:
211
+ str: 随机字符串
212
+ """
213
+ import random
214
+ import string
215
+
216
+ char_map = {
217
+ 'alpha': string.ascii_letters,
218
+ 'numeric': string.digits,
219
+ 'alphanumeric': string.ascii_letters + string.digits,
220
+ 'lower': string.ascii_lowercase,
221
+ 'upper': string.ascii_uppercase,
222
+ }
223
+
224
+ charset = char_map.get(chars, string.ascii_letters + string.digits)
225
+ return ''.join(random.choices(charset, k=int(length)))
226
+
227
+ def _func_random_phone(self, prefix: str = '1') -> str:
228
+ """生成随机手机号
229
+
230
+ Args:
231
+ prefix: 号码前缀
232
+
233
+ Returns:
234
+ str: 随机手机号
235
+ """
236
+ import random
237
+ prefixes = ['13', '14', '15', '16', '17', '18', '19']
238
+ phone_prefix = random.choice(prefixes) if prefix == '1' else prefix
239
+ return phone_prefix + ''.join([str(random.randint(0, 9)) for _ in range(9)])
240
+
241
+ def _func_random_email(self, domain: str = 'test.com') -> str:
242
+ """生成随机邮箱
243
+
244
+ Args:
245
+ domain: 邮箱域名
246
+
247
+ Returns:
248
+ str: 随机邮箱
249
+ """
250
+ username = self._func_random_str('8', 'lower')
251
+ return f"{username}@{domain}"
252
+
253
+ def _func_uuid(self) -> str:
254
+ """生成 UUID
255
+
256
+ Returns:
257
+ str: UUID 字符串
258
+ """
259
+ import uuid
260
+ return str(uuid.uuid4())
261
+
262
+ def _func_timestamp(self, unit: str = 'ms') -> int:
263
+ """获取时间戳
264
+
265
+ Args:
266
+ unit: 单位 (s, ms)
267
+
268
+ Returns:
269
+ int: 时间戳
270
+ """
271
+ import time
272
+ ts = time.time()
273
+ if unit == 'ms':
274
+ return int(ts * 1000)
275
+ return int(ts)
276
+
277
+ def _func_env(self, name: str, default: str = '') -> str:
278
+ """获取环境变量
279
+
280
+ Args:
281
+ name: 环境变量名
282
+ default: 默认值
283
+
284
+ Returns:
285
+ str: 环境变量值
286
+ """
287
+ import os
288
+ return os.environ.get(name, default)
289
+
290
+ def register_function(self, name: str, func) -> None:
291
+ """注册自定义函数
292
+
293
+ Args:
294
+ name: 函数名
295
+ func: 函数对象
296
+ """
297
+ self._functions[name] = func
298
+
299
+ def set_custom_params(self, params: Dict[str, Any]) -> None:
300
+ """设置自定义参数
301
+
302
+ Args:
303
+ params: 参数字典
304
+ """
305
+ self._custom_params.update(params)