iflow-mcp_galaxyxieyu_api-auto-test 0.1.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.
atf/conftest.py ADDED
@@ -0,0 +1,65 @@
1
+ # @time: 2024-09-10
2
+ # @author: xiaoqq
3
+
4
+ import time
5
+ from datetime import datetime
6
+ from atf.core.globals import Globals
7
+ import pytest
8
+
9
+
10
+ # ==================== pytest-html 报告配置 ====================
11
+
12
+ def pytest_configure(config):
13
+ """配置 pytest-html 元数据"""
14
+ config._metadata = getattr(config, '_metadata', {}) or {}
15
+ config._metadata["项目"] = "API Auto Test Framework"
16
+ config._metadata["框架"] = "pytest + pytest-html"
17
+
18
+
19
+ def pytest_html_report_title(report):
20
+ """设置报告标题"""
21
+ report.title = "API 自动化测试报告"
22
+
23
+
24
+ @pytest.hookimpl(hookwrapper=True)
25
+ def pytest_runtest_makereport(item, call):
26
+ """为测试结果添加描述信息"""
27
+ outcome = yield
28
+ report = outcome.get_result()
29
+ report.description = str(item.function.__doc__ or "")
30
+
31
+
32
+ # ==================== 测试结果收集 ====================
33
+
34
+ def pytest_terminal_summary(terminalreporter, exitstatus, config):
35
+ """
36
+ 获取用例执行结果,保存到全局变量
37
+ """
38
+ total = terminalreporter._numcollected
39
+ passed = len([i for i in terminalreporter.stats.get("passed", []) if i.when != "teardown"])
40
+ failed = len([i for i in terminalreporter.stats.get("failed", []) if i.when != "teardown"])
41
+ error = len([i for i in terminalreporter.stats.get("error", []) if i.when != "teardown"])
42
+ skipped = len([i for i in terminalreporter.stats.get("skipped", []) if i.when != "teardown"])
43
+ _start_time = terminalreporter._sessionstarttime
44
+ start_time = datetime.utcfromtimestamp(_start_time).strftime("%Y-%m-%d %H:%M:%S")
45
+ duration = time.time() - _start_time
46
+
47
+ conclusion = "执行通过"
48
+ if failed and error:
49
+ conclusion = "执行失败,包含失败用例和错误用例!"
50
+ elif failed and not error:
51
+ conclusion = "执行失败,包含失败用例!"
52
+ elif not failed and error:
53
+ conclusion = "执行失败,包含报错用例!"
54
+
55
+ results = {
56
+ "conclusion": conclusion,
57
+ "total": total,
58
+ "passed": passed,
59
+ "failed": failed,
60
+ "error": error,
61
+ "skipped": skipped,
62
+ "start_time": start_time,
63
+ "duration": duration
64
+ }
65
+ Globals.set('test_results', results)
atf/core/__init__.py ADDED
@@ -0,0 +1,40 @@
1
+ """核心模块
2
+
3
+ 使用延迟导入,避免在导入 atf.core 包时加载所有依赖。
4
+ """
5
+
6
+ from importlib import import_module
7
+
8
+ __all__ = [
9
+ "Globals",
10
+ "ConfigManager",
11
+ "LogManager",
12
+ "RequestHandler",
13
+ "VariableResolver",
14
+ "AssertHandler",
15
+ "LoginHandler",
16
+ ]
17
+
18
+ _LAZY_IMPORTS = {
19
+ "Globals": "atf.core.globals",
20
+ "ConfigManager": "atf.core.config_manager",
21
+ "LogManager": "atf.core.log_manager",
22
+ "RequestHandler": "atf.core.request_handler",
23
+ "VariableResolver": "atf.core.variable_resolver",
24
+ "AssertHandler": "atf.core.assert_handler",
25
+ "LoginHandler": "atf.core.login_handler",
26
+ }
27
+
28
+
29
+ def __getattr__(name):
30
+ module_path = _LAZY_IMPORTS.get(name)
31
+ if not module_path:
32
+ raise AttributeError(f"module 'atf.core' has no attribute '{name}'")
33
+ module = import_module(module_path)
34
+ value = getattr(module, name)
35
+ globals()[name] = value
36
+ return value
37
+
38
+
39
+ def __dir__():
40
+ return sorted(list(globals().keys()) + list(_LAZY_IMPORTS.keys()))
@@ -0,0 +1,336 @@
1
+ # @time: 2024-08-06
2
+ # @author: xiaoqq
3
+
4
+ import re
5
+ import mysql.connector
6
+ import threading
7
+ from atf.core.log_manager import log
8
+
9
+ class AssertHandler:
10
+ """断言处理器"""
11
+ def __init__(self):
12
+ self.connection = None
13
+ self.lock = threading.Lock()
14
+
15
+ def __enter__(self):
16
+ log.info("Opening database connection")
17
+ return self
18
+
19
+ def __exit__(self, exc_type, exc_val, exc_tb):
20
+ if self.connection:
21
+ log.info("Closing database connection")
22
+ self.connection.close()
23
+
24
+ def handle_assertion(self, asserts, response, db_config=None):
25
+ """
26
+ 验证 assert 列表中的所有断言
27
+ :param asserts: 断言列表
28
+ :param response: 请求返回的 response 对象
29
+ :param db_config: 数据库配置字典,包含 host, user, password, database 等信息
30
+ """
31
+ log.info(f"执行断言: {asserts}")
32
+ # 如果存在 MySQL 相关的断言类型,建立数据库连接
33
+ if any(assertion['type'].startswith('mysql') for assertion in asserts):
34
+ if db_config is None:
35
+ raise ValueError("数据库配置 db_config 不能为空,因为有 MySQL 相关断言")
36
+ self.connection = mysql.connector.connect(**db_config)
37
+
38
+ for assertion in asserts:
39
+ assert_type = assertion.get('type')
40
+ field_path = assertion.get('field')
41
+ expected = assertion.get('expected')
42
+ container = assertion.get('container')
43
+ query = assertion.get('query')
44
+
45
+ # 检查必填字段
46
+ if not field_path and assert_type not in [
47
+ 'mysql_query',
48
+ 'mysql_query_exists',
49
+ 'mysql_query_true',
50
+ 'contains',
51
+ 'contain',
52
+ 'status_code',
53
+ 'status',
54
+ ]:
55
+ log.error(f"断言的 'field' 不能为空: {assertion}")
56
+ raise ValueError(f"断言的 'field' 不能为空: {assertion}")
57
+ if assert_type in ['equal', 'equals', 'not equal', 'not_equal', 'not_equals'] and expected is None:
58
+ log.error(f"断言的 'expected' 不能为空: {assertion}")
59
+ raise ValueError(f"断言的 'expected' 不能为空: {assertion}")
60
+ if assert_type == 'length' and expected is None:
61
+ log.error(f"断言的 'expected' 不能为空: {assertion}")
62
+ raise ValueError(f"断言的 'expected' 不能为空: {assertion}")
63
+ if assert_type in ['in', 'not in', 'not_in'] and container is None:
64
+ log.error(f"断言的 'container' 不能为空: {assertion}")
65
+ raise ValueError(f"断言的 'container' 不能为空: {assertion}")
66
+
67
+ # 获取字段的实际值
68
+ field_value = self.get_field_value(response, field_path) if field_path else None
69
+
70
+ # 处理各种断言类型
71
+ if assert_type in ('status_code', 'status'):
72
+ # status_code 检查 HTTP 状态码(response 可能是 dict 或 Response 对象)
73
+ actual_status = response.get('_status_code') if isinstance(response, dict) else response.status_code
74
+ assert actual_status == expected, f"Expected status code {expected}, but got {actual_status}"
75
+ elif assert_type in ('equal', 'equals'):
76
+ assert field_value == expected, f"Expected {expected}, but got {field_value}"
77
+ elif assert_type in ('not equal', 'not_equal', 'not_equals'):
78
+ assert field_value != expected, f"Expected not {expected}, but got {field_value}"
79
+ elif assert_type in ('exists', 'exist'):
80
+ assert field_value is not None, f"Expected field {field_path} to exist, but got None"
81
+ elif assert_type in ('is_none', 'is None', 'None'):
82
+ assert field_value is None, f"Expected None, but got {field_value}"
83
+ elif assert_type in ('is_not_none', 'is not None', 'not None'):
84
+ assert field_value is not None, f"Expected not None, but got {field_value}"
85
+ elif assert_type == 'length':
86
+ assert field_value is not None, f"Expected field {field_path} not to be None for length assertion"
87
+ try:
88
+ actual_len = len(field_value)
89
+ except TypeError:
90
+ raise ValueError(f"length 断言不支持该类型: field={field_path}, value={field_value}")
91
+ assert actual_len == expected, f"Expected length {expected}, but got {actual_len}"
92
+ elif assert_type == 'in':
93
+ assert field_value in container, f"Expected {field_value} to be in {container}"
94
+ elif assert_type in ('not in', 'not_in'):
95
+ assert field_value not in container, f"Expected {field_value} to be not in {container}"
96
+ elif assert_type in ('contains', 'contain'):
97
+ # contains 不需要 field,直接检查 response 中是否包含 expected
98
+ def _check_contains(obj, target):
99
+ """递归检查 obj 中是否包含 target"""
100
+ if isinstance(obj, dict):
101
+ return any(_check_contains(v, target) for v in obj.values())
102
+ elif isinstance(obj, list):
103
+ return any(_check_contains(item, target) for item in obj)
104
+ elif isinstance(obj, str):
105
+ return target in obj
106
+ else:
107
+ return str(obj) == str(target)
108
+
109
+ assert _check_contains(response, expected), f"Expected {expected} to be in {response}"
110
+ elif assert_type == 'mysql_query':
111
+ self._validate_mysql_query(query, expected)
112
+ elif assert_type == 'mysql_query_exists':
113
+ self._validate_mysql_query_exists(query)
114
+ elif assert_type == 'mysql_query_true':
115
+ self._validate_mysql_query_true(query)
116
+ # SSE 断言类型
117
+ elif assert_type == 'sse_event_count':
118
+ self._validate_sse_event_count(response, assertion)
119
+ elif assert_type == 'sse_contains':
120
+ self._validate_sse_contains(response, assertion)
121
+ elif assert_type == 'sse_event_exists':
122
+ self._validate_sse_event_exists(response, assertion)
123
+ elif assert_type == 'sse_event_field':
124
+ self._validate_sse_event_field(response, assertion)
125
+ elif assert_type == 'sse_last_event':
126
+ self._validate_sse_last_event(response, assertion)
127
+ else:
128
+ raise ValueError(f"未知的断言类型: {assert_type}")
129
+
130
+ log.info(f"断言通过")
131
+
132
+ def get_field_value(self, response, field_path):
133
+ """
134
+ 根据字段路径获取响应中的值,支持数组索引和多级嵌套。
135
+ :param response: 响应数据
136
+ :param field_path: 字段路径,如 data[0].areaName.ids[1].id
137
+ :return: 字段值或 None
138
+ """
139
+ # 使用正则表达式区分数组索引和字段名
140
+ pattern = re.compile(r'([a-zA-Z_][\w]*)\[(\d+)\]|([a-zA-Z_][\w]*)')
141
+ value = response
142
+
143
+ # 找到所有匹配的部分并逐一处理
144
+ for match in pattern.finditer(field_path):
145
+ field, index, simple_field = match.groups()
146
+
147
+ # 如果是数组形式,例如 data[0]
148
+ if field and index is not None:
149
+ value = value.get(field, []) if isinstance(value, dict) else None
150
+ if isinstance(value, list) and len(value) > int(index):
151
+ value = value[int(index)]
152
+ else:
153
+ value = None
154
+
155
+ # 如果是普通字段
156
+ elif simple_field:
157
+ value = value.get(simple_field) if isinstance(value, dict) else None
158
+
159
+ if value is None:
160
+ break
161
+
162
+ return value
163
+
164
+
165
+ def _validate_mysql_query(self, query, expected):
166
+ """
167
+ 执行 MySQL 查询并验证结果是否等于 expected
168
+ :param query: 要执行的 MySQL 查询
169
+ :param expected: 期望值
170
+ """
171
+ result = self._execute_query(query)
172
+ assert result == expected, f"Expected {expected}, but got {result}"
173
+
174
+ def _validate_mysql_query_exists(self, query):
175
+ """
176
+ 执行 MySQL 查询并验证是否返回了至少一行结果
177
+ :param query: 要执行的 MySQL 查询
178
+ """
179
+ result = self._execute_query(query)
180
+ assert result is not None, f"Expected query to return results, but got None"
181
+
182
+ def _validate_mysql_query_true(self, query):
183
+ """
184
+ 执行 MySQL 查询并验证结果是否为真 (存在)
185
+ :param query: 要执行的 MySQL 查询
186
+ """
187
+ result = self._execute_query(query)
188
+ assert bool(result), f"Expected query result to be True, but got {result}"
189
+
190
+ def _execute_query(self, query):
191
+ """
192
+ 执行 MySQL 查询并返回结果
193
+ :param query: 要执行的 MySQL 查询
194
+ :return: 查询结果
195
+ """
196
+ with self.lock:
197
+ cursor = self.connection.cursor()
198
+ cursor.execute(query)
199
+ try:
200
+ result = cursor.fetchone()
201
+ log.info(f"MySQL query executed: {query}, Result: {result}")
202
+ except mysql.connector.Error as err:
203
+ log.error(f"Error executing query: {query}, Error: {err}")
204
+ raise
205
+ cursor.close()
206
+ return result[0] if result else None
207
+
208
+ # ==================== SSE 断言方法 ====================
209
+
210
+ def _validate_sse_event_count(self, response, assertion):
211
+ """
212
+ 验证 SSE 事件数量
213
+ :param response: SSEResponse 对象
214
+ :param assertion: 断言配置,支持 expected (精确), min, max
215
+ """
216
+ count = response.event_count
217
+ expected = assertion.get('expected')
218
+ min_count = assertion.get('min')
219
+ max_count = assertion.get('max')
220
+
221
+ if expected is not None:
222
+ assert count == expected, f"SSE 事件数量期望 {expected},实际 {count}"
223
+ if min_count is not None:
224
+ assert count >= min_count, f"SSE 事件数量至少 {min_count},实际 {count}"
225
+ if max_count is not None:
226
+ assert count <= max_count, f"SSE 事件数量最多 {max_count},实际 {count}"
227
+ log.info(f"SSE 事件数量断言通过: {count}")
228
+
229
+ def _validate_sse_contains(self, response, assertion):
230
+ """
231
+ 验证 SSE 事件中是否包含指定文本
232
+ :param response: SSEResponse 对象
233
+ :param assertion: 断言配置,expected 为要查找的文本
234
+ """
235
+ expected = assertion.get('expected')
236
+ assert response.contains(expected), f"SSE 事件中未找到文本: {expected}"
237
+ log.info(f"SSE 包含断言通过: {expected}")
238
+
239
+ def _validate_sse_event_exists(self, response, assertion):
240
+ """
241
+ 验证是否存在满足条件的 SSE 事件
242
+ :param response: SSEResponse 对象
243
+ :param assertion: 断言配置,支持 event_type, data_contains 等条件
244
+ """
245
+ conditions = {}
246
+ if 'event_type' in assertion:
247
+ conditions['event_type'] = assertion['event_type']
248
+ if 'data_contains' in assertion:
249
+ conditions['data_contains'] = assertion['data_contains']
250
+
251
+ event = response.find_event(**conditions)
252
+ assert event is not None, f"未找到满足条件的 SSE 事件: {conditions}"
253
+ log.info(f"SSE 事件存在断言通过: {conditions}")
254
+
255
+ def _validate_sse_event_field(self, response, assertion):
256
+ """
257
+ 验证指定索引的 SSE 事件的字段值
258
+ :param response: SSEResponse 对象
259
+ :param assertion: 断言配置,index 为事件索引,field 为字段路径,expected 为期望值
260
+ """
261
+ index = assertion.get('index', 0)
262
+ field = assertion.get('field')
263
+ expected = assertion.get('expected')
264
+
265
+ event = response.get_event(index)
266
+ assert event is not None, f"SSE 事件索引 {index} 不存在"
267
+
268
+ # 从 event.data 中获取字段值
269
+ value = self._get_nested_value(event.get('data', {}), field)
270
+ assert value == expected, f"SSE 事件[{index}].{field} 期望 {expected},实际 {value}"
271
+ log.info(f"SSE 事件字段断言通过: [{index}].{field} = {expected}")
272
+
273
+ def _validate_sse_last_event(self, response, assertion):
274
+ """
275
+ 验证最后一个 SSE 事件
276
+ :param response: SSEResponse 对象
277
+ :param assertion: 断言配置,支持 event_type, data_contains, field + expected
278
+ """
279
+ event = response.get_event(-1)
280
+ assert event is not None, "没有收到任何 SSE 事件"
281
+
282
+ if 'event_type' in assertion:
283
+ assert event.get('event') == assertion['event_type'], \
284
+ f"最后事件类型期望 {assertion['event_type']},实际 {event.get('event')}"
285
+
286
+ if 'data_contains' in assertion:
287
+ assert assertion['data_contains'] in str(event.get('data', '')), \
288
+ f"最后事件未包含: {assertion['data_contains']}"
289
+
290
+ if 'field' in assertion and 'expected' in assertion:
291
+ value = self._get_nested_value(event.get('data', {}), assertion['field'])
292
+ assert value == assertion['expected'], \
293
+ f"最后事件 {assertion['field']} 期望 {assertion['expected']},实际 {value}"
294
+
295
+ log.info(f"SSE 最后事件断言通过")
296
+
297
+ def _get_nested_value(self, data, field_path):
298
+ """从嵌套字典中获取值"""
299
+ if not field_path or not isinstance(data, dict):
300
+ return data
301
+ keys = field_path.split('.')
302
+ value = data
303
+ for key in keys:
304
+ if isinstance(value, dict):
305
+ value = value.get(key)
306
+ else:
307
+ return None
308
+ return value
309
+
310
+ if __name__ == '__main__':
311
+ class MockResponse:
312
+ def __init__(self, json_data):
313
+ self._json_data = json_data
314
+
315
+ def json(self):
316
+ return self._json_data
317
+
318
+ assert_var = [
319
+ {
320
+ "type": "equal",
321
+ "field": "code",
322
+ "expected": 0
323
+ },
324
+ {
325
+ "type": "is not None",
326
+ "field": "data.id"
327
+ },
328
+ {
329
+ "type": "equal",
330
+ "field": "message",
331
+ "expected": "success"
332
+ }
333
+ ]
334
+ response_var = {'code': 0, 'message': 'success', 'data': {}}
335
+
336
+ AssertHandler().handle_assertion(asserts=assert_var, response=response_var)
@@ -0,0 +1,111 @@
1
+ # @time: 2024-08-01
2
+ # @author: xiaoqq
3
+
4
+ import os
5
+ import yaml
6
+ from atf.core.globals import Globals
7
+ from atf.core.log_manager import log
8
+
9
+
10
+ class ConfigManager:
11
+ """
12
+ 用于处理 config.yaml 的配置管理类
13
+ """
14
+ _config = None # 类变量,用于存储加载的配置
15
+
16
+ @classmethod
17
+ def load_config(cls):
18
+ if cls._config is not None:
19
+ return cls._config # 如果已加载,则直接返回
20
+
21
+ # config.yaml 位于项目根目录,core/ 在 atf/ 之下,需要返回两级
22
+ config_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "../../config.yaml")
23
+ try:
24
+ with open(config_path, 'r') as file:
25
+ cls._config = yaml.safe_load(file)
26
+ if cls._config is None:
27
+ log.error(f"{config_path} 未找到配置信息")
28
+ raise ValueError(f"{config_path} 未找到配置信息")
29
+ log.info("读取工程配置信息成功")
30
+
31
+ # 将DingTalk配置信息存入Globals
32
+ dingtalk_config = cls._config.get('notifications').get("dingtalk")
33
+ Globals.set("dingtalk", dingtalk_config)
34
+
35
+ return cls._config
36
+ except FileNotFoundError:
37
+ log.error(f"未找到配置文件: {config_path}")
38
+ raise FileNotFoundError(f"未找到配置文件: {config_path}")
39
+ except yaml.YAMLError as e:
40
+ log.error(f"YAML 配置文件解析错误: {e}")
41
+ raise ValueError(f"YAML 配置文件解析失败: {e}")
42
+
43
+ @classmethod
44
+ def get_project_env_config(cls, project_name, env):
45
+ """
46
+ 获取指定项目和环境的配置信息
47
+ :param project_name: 项目名称
48
+ :param env: 环境名称 (如 test, pre, online)
49
+ :return: 项目的环境配置字典或 None
50
+ """
51
+ config = cls.load_config()
52
+ project_config = config["projects"].get(project_name, None)
53
+
54
+ if project_config is None:
55
+ return None
56
+
57
+ final_config = {}
58
+
59
+ if "is_scene" in project_config and project_config["is_scene"]:
60
+ for sub_project in project_config.get("sub_projects", []):
61
+ sub_project_config = config["projects"].get(sub_project, None)
62
+ if sub_project_config is None:
63
+ log.warning(f"子项目{sub_project}在配置文件中不存在")
64
+ continue
65
+
66
+ sub_project_env_config = sub_project_config.get(env, None)
67
+ if sub_project_env_config:
68
+ cls.validate_config(sub_project_env_config)
69
+ final_config[sub_project] = sub_project_env_config
70
+ else:
71
+ log.warning(f"子项目 {sub_project} 中未找到环境 {env} 的配置信息,返回 None")
72
+ final_config[sub_project] = None
73
+ # 存储子项目配置信息到Globals中
74
+ Globals.set(sub_project, sub_project_env_config)
75
+ else:
76
+ project_env_config = project_config.get(env, None)
77
+ if project_env_config:
78
+ cls.validate_config(project_env_config)
79
+ final_config[project_name] = project_env_config
80
+ else:
81
+ log.warning(f"项目 {project_name} 中未找到环境 {env} 的配置信息,返回 None")
82
+ final_config[project_name] = None
83
+ # 存储项目配置信息到Globals中
84
+ Globals.set(project_name, project_env_config)
85
+
86
+ return final_config
87
+
88
+ @staticmethod
89
+ def validate_config(project_env_config):
90
+ """
91
+ 验证配置文件的项目环境配置中是否有必需字段
92
+ :param project_env_config: 项目的环境配置字典
93
+ """
94
+ required_keys = ['host', 'is_need_login'] # 需要验证的关键字段
95
+ for key in required_keys:
96
+ if key not in project_env_config:
97
+ log.error(f"{project_env_config} 中缺少必需配置项 {key}")
98
+ raise ValueError(f"{project_env_config} 中缺少必需配置项 {key}")
99
+
100
+ if project_env_config['is_need_login']:
101
+ login_config = project_env_config.get('login', {})
102
+ required_login_keys = ['url', 'method', 'data']
103
+ for key in required_login_keys:
104
+ if key not in login_config:
105
+ log.error(f"{project_env_config} 中登录配置缺失或不完整")
106
+ raise ValueError(f"{project_env_config} 中登录配置缺失或不完整")
107
+
108
+
109
+ if __name__ == '__main__':
110
+ conf = ConfigManager.get_project_env_config(project_name="merchant", env="pre")
111
+ print(conf)
atf/core/globals.py ADDED
@@ -0,0 +1,52 @@
1
+ # @time: 2024-08-16
2
+ # @author: xiaoqq
3
+
4
+ import threading
5
+
6
+ class Globals:
7
+ """
8
+ 全局变量管理,线程安全
9
+ """
10
+ _instance = None
11
+ _lock = threading.Lock()
12
+ _data = {}
13
+
14
+ def __new__(cls):
15
+ if cls._instance is None:
16
+ with cls._lock:
17
+ if cls._instance is None:
18
+ cls._instance = super(Globals, cls).__new__(cls)
19
+ return cls._instance
20
+
21
+ @classmethod
22
+ def get_data(cls):
23
+ with cls._lock:
24
+ return cls._data.copy()
25
+
26
+ @classmethod
27
+ def set(cls, key, value):
28
+ with cls._lock:
29
+ # 仅在数据发生变化时才进行设置
30
+ if cls._data.get(key) != value:
31
+ cls._data[key] = value
32
+
33
+ @classmethod
34
+ def get(cls, key):
35
+ with cls._lock:
36
+ return cls._data.get(key)
37
+
38
+ @classmethod
39
+ def update(cls, key, sub_key, sub_value):
40
+ with cls._lock:
41
+ if key in cls._data:
42
+ if isinstance(cls._data[key], dict):
43
+ cls._data[key][sub_key] = sub_value
44
+ else:
45
+ raise ValueError(f"Cannot update non-dict item in Globals: {key}")
46
+ else:
47
+ raise KeyError(f"Key {key} not found in Globals")
48
+
49
+ @classmethod
50
+ def clear(cls):
51
+ with cls._lock:
52
+ cls._data.clear()
@@ -0,0 +1,52 @@
1
+ # @time: 2024-08-01
2
+ # @author: xiaoqq
3
+
4
+ import os
5
+ from datetime import datetime
6
+ from loguru import logger
7
+
8
+ class LogManager:
9
+ '''
10
+ 日志记录器封装
11
+ '''
12
+
13
+ @staticmethod
14
+ def setup_logging():
15
+ root_path = os.path.dirname(os.path.abspath(__file__)) # 根目录
16
+ log_dir = os.path.join(root_path, "../logs")
17
+ if not os.path.exists(log_dir):
18
+ os.makedirs(log_dir)
19
+
20
+ log_name = datetime.now().strftime("%Y-%m-%d") # 日志文件名命名格式为“年-月-日”
21
+ sink = os.path.join(log_dir, "{}.log".format(log_name)) # 日志文件路径
22
+ level = "DEBUG" # 记录的最低日志级别为DEBUG
23
+ encoding = "utf-8" # 写入日志文件时编码格式为utf-8
24
+ enqueue = True # 多线程多进程时保证线程安全
25
+ rotation = "500MB" # 日志文件最大为500MB,超过则新建文件记录日志
26
+ retention = "1 week" # 日志保留时长为1星期,超时则清除
27
+
28
+ logger.add(
29
+ sink=sink,
30
+ level=level,
31
+ encoding=encoding,
32
+ enqueue=enqueue,
33
+ rotation=rotation,
34
+ retention=retention
35
+ )
36
+
37
+ @staticmethod
38
+ def get_logger():
39
+ return logger
40
+
41
+ # 在工程开始时初始化设置日志记录器
42
+ LogManager.setup_logging()
43
+
44
+ # 获取日志记录器实例,用于其他模块调用
45
+ log = LogManager.get_logger()
46
+
47
+
48
+ if __name__ == '__main__':
49
+ log.debug("debug消息")
50
+ log.info("info消息")
51
+ log.warning("warning消息")
52
+ log.error("error消息")