dataops-sdk 0.3.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.

Potentially problematic release.


This version of dataops-sdk might be problematic. Click here for more details.

Files changed (79) hide show
  1. dataops_sdk/__init__.py +84 -0
  2. dataops_sdk/constants.py +6 -0
  3. dataops_sdk/exceptions.py +31 -0
  4. dataops_sdk/runner/__init__.py +46 -0
  5. dataops_sdk/runner/datasource_factory.py +83 -0
  6. dataops_sdk/runner/schemas/nacos/__init__.py +5 -0
  7. dataops_sdk/runner/schemas/nacos/config.py +40 -0
  8. dataops_sdk/runner/schemas/nacos/constants.py +235 -0
  9. dataops_sdk/runner/schemas/nacos/instance.py +103 -0
  10. dataops_sdk/runner/schemas/nacos/nacos_exception.py +44 -0
  11. dataops_sdk/runner/schemas/nacos/naming_param.py +62 -0
  12. dataops_sdk/runner/schemas/nacos/naming_request.py +84 -0
  13. dataops_sdk/runner/schemas/nacos/naming_response.py +50 -0
  14. dataops_sdk/runner/schemas/nacos/preserved_metadata_key.py +10 -0
  15. dataops_sdk/runner/schemas/nacos/service.py +119 -0
  16. dataops_sdk/runner/utils/__init__.py +42 -0
  17. dataops_sdk/runner/utils/common.py +209 -0
  18. dataops_sdk/runner/utils/dbConn_base.py +256 -0
  19. dataops_sdk/runner/utils/dbConn_sync.py +485 -0
  20. dataops_sdk/runner/utils/duckdb_manager.py +203 -0
  21. dataops_sdk/runner/utils/file_transfer_base.py +307 -0
  22. dataops_sdk/runner/utils/ftp_file_transfer.py +242 -0
  23. dataops_sdk/runner/utils/log.py +50 -0
  24. dataops_sdk/runner/utils/nacos.py +96 -0
  25. dataops_sdk/runner/utils/neo4j_sync.py +236 -0
  26. dataops_sdk/runner/utils/queue_base.py +224 -0
  27. dataops_sdk/runner/utils/queue_kafka.py +150 -0
  28. dataops_sdk/runner/utils/sftp_file_transfer.py +243 -0
  29. dataops_sdk/schemas/__init__.py +32 -0
  30. dataops_sdk/schemas/alarm.py +38 -0
  31. dataops_sdk/schemas/api.py +37 -0
  32. dataops_sdk/schemas/data_table.py +437 -0
  33. dataops_sdk/schemas/datasource.py +26 -0
  34. dataops_sdk/schemas/dict_.py +13 -0
  35. dataops_sdk/schemas/enums.py +120 -0
  36. dataops_sdk/schemas/group.py +40 -0
  37. dataops_sdk/schemas/hazard.py +151 -0
  38. dataops_sdk/schemas/meta.py +175 -0
  39. dataops_sdk/schemas/query.py +175 -0
  40. dataops_sdk/schemas/report.py +22 -0
  41. dataops_sdk/schemas/skill.py +70 -0
  42. dataops_sdk/schemas/table.py +65 -0
  43. dataops_sdk/schemas/user.py +52 -0
  44. dataops_sdk/schemas/widget.py +651 -0
  45. dataops_sdk/schemas/workflow.py +63 -0
  46. dataops_sdk/services/__init__.py +110 -0
  47. dataops_sdk/services/alarm.py +324 -0
  48. dataops_sdk/services/api.py +28 -0
  49. dataops_sdk/services/auth.py +120 -0
  50. dataops_sdk/services/data_table.py +485 -0
  51. dataops_sdk/services/dataset.py +127 -0
  52. dataops_sdk/services/datasource.py +57 -0
  53. dataops_sdk/services/dict_.py +69 -0
  54. dataops_sdk/services/hazard.py +260 -0
  55. dataops_sdk/services/meta.py +523 -0
  56. dataops_sdk/services/metrics.py +42 -0
  57. dataops_sdk/services/ref.py +66 -0
  58. dataops_sdk/services/report.py +36 -0
  59. dataops_sdk/services/skill.py +276 -0
  60. dataops_sdk/services/table.py +340 -0
  61. dataops_sdk/services/widget.py +143 -0
  62. dataops_sdk/services/workflow.py +102 -0
  63. dataops_sdk/session.py +292 -0
  64. dataops_sdk/tiptap/__init__.py +30 -0
  65. dataops_sdk/tiptap/builder.py +75 -0
  66. dataops_sdk/tiptap/constants.py +133 -0
  67. dataops_sdk/tiptap/markdown_parser.py +294 -0
  68. dataops_sdk/tiptap/models.py +43 -0
  69. dataops_sdk/tiptap/parse_utils.py +889 -0
  70. dataops_sdk/tiptap/render_utils.py +846 -0
  71. dataops_sdk/tiptap/renderers.py +51 -0
  72. dataops_sdk/tiptap/tool.py +629 -0
  73. dataops_sdk/utils/__init__.py +15 -0
  74. dataops_sdk/utils/common.py +211 -0
  75. dataops_sdk/utils/utils.py +120 -0
  76. dataops_sdk-0.3.0.dist-info/METADATA +25 -0
  77. dataops_sdk-0.3.0.dist-info/RECORD +79 -0
  78. dataops_sdk-0.3.0.dist-info/WHEEL +5 -0
  79. dataops_sdk-0.3.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,307 @@
1
+ """
2
+ 文件传输抽象基类模块。
3
+
4
+ 提供统一的 FTP/SFTP 文件传输接口,支持上传、下载、列表等操作。
5
+
6
+ Classes:
7
+ FileTransferBase: 文件传输操作抽象基类
8
+
9
+ Example:
10
+ >>> class MyFileTransfer(FileTransferBase):
11
+ ... def post_init(self):
12
+ ... pass # 初始化具体传输连接
13
+ ... def upload(self, local_path, remote_name):
14
+ ... pass
15
+ ... def download(self, remote_name, local_path):
16
+ ... pass
17
+ ... def list_files(self, pattern=None):
18
+ ... return []
19
+ ... def delete(self, remote_name):
20
+ ... pass
21
+ ... def exists(self, remote_name):
22
+ ... pass
23
+ ... def ping(self):
24
+ ... pass
25
+ ... def close(self):
26
+ ... pass
27
+ ...
28
+ >>> with MyFileTransfer(host="localhost", port=21,
29
+ ... user="user", password="pass") as ft:
30
+ ... ft.upload("/local/file.txt", "remote_file.txt")
31
+ """
32
+
33
+ import os
34
+ from abc import ABC, abstractmethod
35
+ from logging import Logger
36
+ from typing import BinaryIO
37
+
38
+ from .log import getLogger
39
+
40
+
41
+ class FileTransferBase(ABC):
42
+ """
43
+ 文件传输抽象基类。
44
+
45
+ 定义文件传输的标准接口,包括上传、下载、列表、删除和连接检查等方法。
46
+
47
+ 继承此类需要实现以下抽象方法:
48
+ - post_init: 初始化具体传输连接对象
49
+ - upload: 上传文件到远程服务器
50
+ - download: 从远程服务器下载文件
51
+ - list_files: 列出远程文件
52
+ - delete: 删除远程文件
53
+ - exists: 检查远程文件是否存在
54
+ - ping: 检查连接是否存活
55
+ - close: 关闭连接
56
+
57
+ 可以作为上下文管理器使用,会自动在退出时关闭连接。
58
+
59
+ Attributes:
60
+ logger: 日志记录器,用于记录文件传输操作日志。
61
+
62
+ Example:
63
+ >>> with FTPFileTransfer(host="ftp.example.com", port=21,
64
+ ... user="user", password="pass") as ftp:
65
+ ... ftp.upload("/local/file.txt", "remote_file.txt")
66
+ ... files = ftp.list_files("*.txt")
67
+ """
68
+
69
+ def __init__(
70
+ self,
71
+ host: str,
72
+ user: str,
73
+ password: str,
74
+ remote_path: str = "/",
75
+ port: int | str = 22,
76
+ name: str | None = None,
77
+ ):
78
+ """
79
+ 初始化文件传输基类。
80
+
81
+ 将传入的连接参数保存为实例属性,并初始化日志记录器。
82
+ 子类应在 post_init 方法中创建实际的文件传输连接。
83
+
84
+ Args:
85
+ host: 文件服务器主机地址。
86
+ port: 文件服务器端口号。
87
+ user: 文件服务器用户名。
88
+ password: 文件服务器密码。
89
+ remote_path: 远程基础目录,默认为 "/"。
90
+ name: 连接名称。
91
+ """
92
+ self._host = host
93
+ self._port = int(port)
94
+ self._user = user
95
+ self._password = password
96
+ self._remote_path = remote_path
97
+
98
+ self._name = name or f"file_transfer_{self._host}"
99
+
100
+ self._logger = getLogger(name)
101
+
102
+ self.post_init()
103
+
104
+ @abstractmethod
105
+ def post_init(self):
106
+ """
107
+ 初始化具体文件传输连接对象。
108
+
109
+ 子类应在此方法中创建实际的 FTP/SFTP 连接。
110
+ 该方法在 __init__ 中被调用,用于延迟初始化连接。
111
+
112
+ Returns:
113
+ None
114
+ """
115
+ pass
116
+
117
+ @abstractmethod
118
+ def upload(self, fp: BinaryIO | str | bytes, remote_name: str) -> bool:
119
+ """
120
+ 上传文件到远程服务器。
121
+
122
+ 将本地文件对象上传到远程服务器,存储在 remote_path 目录下。
123
+
124
+ Args:
125
+ fp: 文件对象,支持任意 IO 类型(如 BytesIO, FileIO 等)。
126
+ remote_name: 远程文件名(不含路径)。
127
+
128
+ Returns:
129
+ bool: 上传成功返回 True,否则返回 False。
130
+ """
131
+ pass
132
+
133
+ @abstractmethod
134
+ def download(self, remote_name: str, local_path: str) -> bool:
135
+ """
136
+ 从远程服务器下载文件。
137
+
138
+ 从远程服务器下载文件到本地路径。
139
+
140
+ Args:
141
+ remote_name: 远程文件名(不含路径)。
142
+ local_path: 本地保存路径(包含文件名)。
143
+
144
+ Returns:
145
+ bool: 下载成功返回 True,否则返回 False。
146
+ """
147
+ pass
148
+
149
+ @abstractmethod
150
+ def list_files(self, pattern: str | None = None) -> list[str]:
151
+ """
152
+ 列出远程目录下的文件。
153
+
154
+ Args:
155
+ pattern: 可选的 glob 模式,用于过滤文件名。
156
+
157
+ Returns:
158
+ list[str]: 文件名列表。
159
+ """
160
+ pass
161
+
162
+ @abstractmethod
163
+ def delete(self, remote_name: str) -> bool:
164
+ """
165
+ 删除远程文件。
166
+
167
+ Args:
168
+ remote_name: 远程文件名(不含路径)。
169
+
170
+ Returns:
171
+ bool: 删除成功返回 True,否则返回 False。
172
+ """
173
+ pass
174
+
175
+ @abstractmethod
176
+ def exists(self, remote_name: str) -> bool:
177
+ """
178
+ 检查远程文件是否存在。
179
+
180
+ Args:
181
+ remote_name: 远程文件名(不含路径)。
182
+
183
+ Returns:
184
+ bool: 文件存在返回 True,否则返回 False。
185
+ """
186
+ pass
187
+
188
+ @abstractmethod
189
+ def ping(self) -> bool:
190
+ """
191
+ 检查连接是否存活。
192
+
193
+ Returns:
194
+ bool: 连接正常返回 True,否则返回 False。
195
+ """
196
+ pass
197
+
198
+ @abstractmethod
199
+ def close(self):
200
+ """
201
+ 关闭文件传输连接。
202
+
203
+ 释放连接资源,应在不再需要连接时调用。
204
+ 支持多次调用,子类应实现幂等性。
205
+ """
206
+ pass
207
+
208
+ @property
209
+ def name(self) -> str:
210
+ """
211
+ 文件传输连接名称属性。
212
+
213
+ 返回连接的名称,用于标识连接。
214
+
215
+ Returns:
216
+ str: 文件传输连接名称。
217
+ """
218
+ return self._name
219
+
220
+ @property
221
+ def logger(self) -> Logger:
222
+ """
223
+ 日志记录器属性。
224
+
225
+ 提供对内部日志记录器的只读访问,用于记录文件传输操作日志。
226
+
227
+ Returns:
228
+ Logger: Python logging 模块的日志记录器实例。
229
+ """
230
+ return self._logger
231
+
232
+ @property
233
+ def host(self) -> str:
234
+ """
235
+ 主机地址属性。
236
+
237
+ Returns:
238
+ str: 文件服务器主机地址。
239
+ """
240
+ return self._host
241
+
242
+ @property
243
+ def port(self) -> int:
244
+ """
245
+ 端口属性。
246
+
247
+ Returns:
248
+ int: 文件服务器端口号。
249
+ """
250
+ return self._port
251
+
252
+ @property
253
+ def user(self) -> str:
254
+ """
255
+ 用户名属性。
256
+
257
+ Returns:
258
+ str: 文件服务器用户名。
259
+ """
260
+ return self._user
261
+
262
+ @property
263
+ def remote_path(self) -> str:
264
+ """
265
+ 远程路径属性。
266
+
267
+ Returns:
268
+ str: 远程基础目录。
269
+ """
270
+ return self._remote_path
271
+
272
+ def __enter__(self):
273
+ """
274
+ 上下文管理器入口方法。
275
+
276
+ 使 FileTransferBase 的子类可以作为 with 语句使用。
277
+ 进入 with 块时返回 self 引用。
278
+
279
+ Returns:
280
+ self: 返回当前实例的引用。
281
+ """
282
+ return self
283
+
284
+ def __exit__(self, exc_type, exc_val, exc_tb):
285
+ """
286
+ 上下文管理器退出方法。
287
+
288
+ 退出 with 块时自动调用,异常会被记录后关闭连接。
289
+ 确保文件传输连接被正确关闭。
290
+
291
+ Args:
292
+ exc_type: 异常类型,若无异常则为 None。
293
+ exc_val: 异常值/实例,若无异常则为 None。
294
+ exc_tb: 异常回溯对象,若无异常则为 None。
295
+ """
296
+ if exc_type:
297
+ self._logger.exception(exc_val)
298
+ self.close()
299
+
300
+ def __del__(self):
301
+ """
302
+ 析构函数。
303
+
304
+ 当对象被垃圾回收时自动调用,确保连接被关闭。
305
+ 这是一个安全网机制,不能替代显式调用 close()。
306
+ """
307
+ self.close()
@@ -0,0 +1,242 @@
1
+ """
2
+ 基于 FTP 的文件传输实现。
3
+
4
+ 继承自 FileTransferBase,使用 Python 内置的 ftplib 实现 FTP 文件传输。
5
+ ftplib 是 Python 标准库,无需额外安装依赖。
6
+
7
+ Classes:
8
+ FTPFileTransfer: 基于 ftplib 的 FTP 文件传输类
9
+
10
+ Example:
11
+ >>> with FTPFileTransfer(host="ftp.example.com", port=21,
12
+ ... user="ftp_user", password="ftp_password",
13
+ ... remote_path="/uploads") as ftp:
14
+ ... ftp.upload("/local/file.txt", "remote_file.txt")
15
+ ... files = ftp.list_files("*.txt")
16
+ ... if ftp.exists("remote_file.txt"):
17
+ ... ftp.download("remote_file.txt", "/local/downloaded.txt")
18
+ """
19
+
20
+ import fnmatch
21
+ from ftplib import FTP, error_perm
22
+ from io import BytesIO
23
+ from pathlib import Path
24
+ from typing import BinaryIO
25
+
26
+ from .file_transfer_base import FileTransferBase
27
+
28
+
29
+ class FTPFileTransfer(FileTransferBase):
30
+ """
31
+ 基于 ftplib 的 FTP 文件传输类。
32
+
33
+ 支持:
34
+ - 上传/下载文件
35
+ - 列出远程文件(支持 glob 模式过滤)
36
+ - 删除远程文件
37
+ - 检查文件是否存在
38
+ - 连接健康检查
39
+ - 上下文管理器(自动关闭连接)
40
+
41
+ 继承自 FileTransferBase,实现所有抽象方法。
42
+
43
+ Attributes:
44
+ _ftp: ftplib.FTP 实例
45
+
46
+ Example:
47
+ >>> with FTPFileTransfer(host="ftp.example.com", port=21,
48
+ ... user="ftp_user", password="ftp_password",
49
+ ... remote_path="/uploads") as ftp:
50
+ ... ftp.upload("/local/file.txt", "remote_file.txt")
51
+ ... files = ftp.list_files("*.txt")
52
+ """
53
+
54
+ def __init__(
55
+ self,
56
+ host: str,
57
+ user: str,
58
+ password: str,
59
+ remote_path: str = "/",
60
+ port: int | str = 21,
61
+ name: str | None = None,
62
+ passive: bool = True,
63
+ ):
64
+ """
65
+ 初始化 FTP 文件传输。
66
+
67
+ Args:
68
+ host: FTP 服务器主机地址。
69
+ user: FTP 服务器用户名。
70
+ password: FTP 服务器密码。
71
+ remote_path: 远程基础目录,默认为 "/"。
72
+ port: FTP 服务器端口号,默认为 21。
73
+ name: 连接名称,用于日志标识。
74
+ passive: 是否使用被动模式,默认为 True。
75
+ True = 被动模式(客户端连接数据端口),适合服务器在防火墙后。
76
+ False = 主动模式(服务器连接客户端数据端口),适合客户端在防火墙后。
77
+ """
78
+ super().__init__(host, user, password, remote_path, port, name)
79
+ self._passive = passive
80
+
81
+ def post_init(self):
82
+ """
83
+ 初始化 FTP 连接。
84
+
85
+ 连接到 FTP 服务器并切换到远程基础目录。
86
+
87
+ Returns:
88
+ None
89
+ """
90
+ self.logger.info(f"Connecting to FTP server: {self._host}:{self._port}")
91
+
92
+ self._ftp: FTP = FTP()
93
+ self._ftp.connect(self._host, self._port)
94
+ self._ftp.login(self._user, self._password)
95
+ self._ftp.keepalive()
96
+ self._ftp.set_pasv(self._passive)
97
+
98
+ # 切换到远程基础目录
99
+ self._ftp.cwd(self._remote_path)
100
+
101
+ self.logger.info(f"FTP connection established to {self._host}")
102
+
103
+ def upload(self, fp: BinaryIO | str | bytes, remote_name: str) -> bool:
104
+ """
105
+ 上传文件到 FTP 服务器。
106
+
107
+ Args:
108
+ fp: 文件对象,支持任意 IO 类型(如 BytesIO, FileIO 等)。
109
+ remote_name: 远程文件名。
110
+
111
+ Returns:
112
+ bool: 上传成功返回 True,否则返回 False。
113
+ """
114
+ try:
115
+ if isinstance(fp, str):
116
+ fp = BytesIO(fp.encode("utf-8"))
117
+ elif isinstance(fp, bytes):
118
+ fp = BytesIO(fp)
119
+ self._ftp.storbinary(f"STOR {remote_name}", fp)
120
+ self.logger.info(f"Uploaded: -> {remote_name}")
121
+ return True
122
+ except Exception as e:
123
+ self.logger.error(f"Failed to upload: {e}")
124
+ return False
125
+
126
+ def download(self, remote_name: str, local_path: str) -> bool:
127
+ """
128
+ 从 FTP 服务器下载文件。
129
+
130
+ Args:
131
+ remote_name: 远程文件名。
132
+ local_path: 本地保存路径。
133
+
134
+ Returns:
135
+ bool: 下载成功返回 True,否则返回 False。
136
+ """
137
+ try:
138
+ # 确保本地目录存在
139
+ Path(local_path).parent.mkdir(parents=True, exist_ok=True)
140
+
141
+ with open(local_path, "wb") as f:
142
+ self._ftp.retrbinary(f"RETR {remote_name}", f.write)
143
+ self.logger.info(f"Downloaded: {remote_name} -> {local_path}")
144
+ return True
145
+ except Exception as e:
146
+ self.logger.error(f"Failed to download {remote_name}: {e}")
147
+ return False
148
+
149
+ def list_files(self, pattern: str | None = None) -> list[str]:
150
+ """
151
+ 列出 FTP 远程目录下的文件。
152
+
153
+ Args:
154
+ pattern: 可选的 glob 模式,用于过滤文件名。
155
+
156
+ Returns:
157
+ list[str]: 文件名列表。
158
+ """
159
+ try:
160
+ files = self._ftp.nlst()
161
+ if pattern:
162
+ files = [f for f in files if fnmatch.fnmatch(f, pattern)]
163
+ return files
164
+ except error_perm as e:
165
+ # No files or empty directory
166
+ self.logger.debug(f"No files found or empty directory: {e}")
167
+ return []
168
+ except Exception as e:
169
+ self.logger.error(f"Failed to list files: {e}")
170
+ return []
171
+
172
+ def delete(self, remote_name: str) -> bool:
173
+ """
174
+ 删除 FTP 服务器上的文件。
175
+
176
+ Args:
177
+ remote_name: 远程文件名。
178
+
179
+ Returns:
180
+ bool: 删除成功返回 True,否则返回 False。
181
+ """
182
+ try:
183
+ self._ftp.delete(remote_name)
184
+ self.logger.info(f"Deleted: {remote_name}")
185
+ return True
186
+ except Exception as e:
187
+ self.logger.error(f"Failed to delete {remote_name}: {e}")
188
+ return False
189
+
190
+ def exists(self, remote_name: str) -> bool:
191
+ """
192
+ 检查 FTP 服务器上文件是否存在。
193
+
194
+ Args:
195
+ remote_name: 远程文件名。
196
+
197
+ Returns:
198
+ bool: 文件存在返回 True,否则返回 False。
199
+ """
200
+ try:
201
+ # Use SIZE command - returns size if exists, raises error if not
202
+ self._ftp.size(remote_name)
203
+ return True
204
+ except error_perm:
205
+ return False
206
+ except Exception as e:
207
+ self.logger.error(f"Failed to check existence of {remote_name}: {e}")
208
+ return False
209
+
210
+ def ping(self) -> bool:
211
+ """
212
+ 检查 FTP 连接是否存活。
213
+
214
+ Returns:
215
+ bool: 连接正常返回 True,否则返回 False。
216
+ """
217
+ try:
218
+ self._ftp.voidcmd("NOOP")
219
+ return True
220
+ except Exception as e:
221
+ self.logger.warning(f"FTP ping failed: {e}")
222
+ return False
223
+
224
+ def close(self):
225
+ """
226
+ 关闭 FTP 连接。
227
+
228
+ 关闭 FTP 连接,释放资源。
229
+ 支持多次调用,子类应实现幂等性。
230
+ """
231
+ if hasattr(self, "_ftp") and self._ftp is not None:
232
+ try:
233
+ self._ftp.quit()
234
+ self.logger.info("FTP connection closed")
235
+ except Exception as e:
236
+ self.logger.error(f"Failed to close FTP connection: {e}")
237
+ self._ftp = None
238
+
239
+ @property
240
+ def passive(self) -> bool:
241
+ """被动模式"""
242
+ return self._passive
@@ -0,0 +1,50 @@
1
+ """日志模块。
2
+
3
+ 提供日志记录器创建功能,支持控制台和文件输出。
4
+ 文件日志按天轮转,保留 30 天。
5
+
6
+ Functions:
7
+ getLogger: 获取或创建日志记录器
8
+ safe_str: 安全字符串转换,处理编码问题
9
+
10
+ Example:
11
+ >>> logger = getLogger(__name__)
12
+ >>> logger.info("Hello, world!")
13
+ """
14
+
15
+ import os
16
+ from logging import INFO, Formatter, StreamHandler
17
+ from logging import getLogger as _getLogger
18
+ from logging.handlers import RotatingFileHandler
19
+
20
+ from dataops_sdk.utils import get_log_path
21
+
22
+
23
+ def getLogger(name: str | None = None, level: int = INFO):
24
+ _logger = _getLogger(name)
25
+ if _logger.hasHandlers():
26
+ return _logger
27
+
28
+ formatter = Formatter(
29
+ "%(asctime)s [%(levelname)s] %(message)s",
30
+ datefmt="%Y-%m-%d %H:%M:%S",
31
+ )
32
+ task_id = os.getenv("TASK_ID", None)
33
+ if task_id:
34
+ log_file = get_log_path(task_id)
35
+ # 文件处理器,按天分割(每天午夜轮转),保留30天的备份(约一个月)
36
+ # 使用 UTF-8 编码,避免 Unicode 编码错误
37
+ file_handler = RotatingFileHandler(
38
+ log_file, maxBytes=100 * 1024 * 1024, backupCount=1000, encoding="utf-8"
39
+ )
40
+ file_handler.setFormatter(formatter)
41
+ file_handler.setLevel(level)
42
+ _logger.addHandler(file_handler)
43
+
44
+ _handler = StreamHandler()
45
+ _handler.setFormatter(formatter)
46
+ _handler.setLevel(level)
47
+
48
+ _logger.addHandler(_handler)
49
+ _logger.setLevel(level)
50
+ return _logger
@@ -0,0 +1,96 @@
1
+ """Nacos 服务发现模块。
2
+
3
+ 提供从 Nacos 发现服务实例并返回 HTTP 客户端的功能。
4
+
5
+ Functions:
6
+ get_service_instance: 从 Nacos 发现服务实例并返回 HTTP 客户端
7
+
8
+ Example:
9
+ >>> client = await get_service_instance("my-service")
10
+ >>> response = await client.get("/api")
11
+ """
12
+
13
+ import atexit
14
+ import random
15
+ from collections.abc import Mapping
16
+
17
+ import httpx2
18
+ from dataops_sdk.runner.schemas.nacos import NacosSettings, Service
19
+ from dataops_sdk.runner.schemas.nacos.constants import Constants
20
+
21
+
22
+ def get_service_instance(
23
+ service_name: str,
24
+ group_name: str = Constants.DEFAULT_GROUP,
25
+ clusters: list[str] | None = None,
26
+ headers: Mapping[str, str] | None = None,
27
+ nacos_addrs: str | None = None,
28
+ nacos_user: str | None = None,
29
+ nacos_password: str | None = None,
30
+ namespace_id: str | None = None,
31
+ ) -> httpx2.Client:
32
+ """
33
+ 从Nacos发现服务实例并返回一个HTTP客户端
34
+
35
+ Args:
36
+ service_name (str): Nacos服务名称
37
+ group_name (str, None): Nacos服务分组,默认为 "DEFAULT_GROUP
38
+ clusters (list[str], None): Nacos服务集群,默认为空
39
+ headers: 可选的 HTTP 请求头
40
+ nacos_addrs: Nacos 服务地址,覆盖环境变量
41
+ nacos_user: Nacos 用户名,覆盖环境变量
42
+ nacos_password: Nacos 密码,覆盖环境变量
43
+ namespace_id: Nacos 命名空间 ID,覆盖环境变量
44
+
45
+ Returns:
46
+ httpx2.Client: 连接到选定服务实例的HTTP客户端
47
+
48
+ Raises:
49
+ NacosException: Nacos客户端错误(可能是连接问题、认证失败等)
50
+ RuntimeError: 如果没有可用的服务实例
51
+
52
+ Example:
53
+ >>> client = await get_service_instance("my-service")
54
+ >>> response = await client.get("/api")
55
+ """
56
+ if clusters is None:
57
+ clusters = []
58
+ config = NacosSettings()
59
+
60
+ if not nacos_user:
61
+ nacos_user = config.nacos_user
62
+
63
+ if not nacos_password:
64
+ nacos_password = config.nacos_password
65
+
66
+ if not nacos_addrs:
67
+ nacos_addrs = config.nacos_addrs
68
+
69
+ if not group_name:
70
+ group_name = config.nacos_group_name or Constants.DEFAULT_GROUP
71
+
72
+ if not namespace_id:
73
+ namespace_id = config.nacos_discovery_namespace
74
+ assert namespace_id, "namespace_id must be provided"
75
+
76
+ resp = httpx2.get(
77
+ f"http://{nacos_addrs}/nacos/v1/ns/instance/list",
78
+ params={
79
+ "username": nacos_user,
80
+ "password": nacos_password,
81
+ "serviceName": service_name,
82
+ "namespaceId": namespace_id,
83
+ "groupName": group_name,
84
+ "healthyOnly": "true",
85
+ },
86
+ )
87
+ service = Service.model_validate_json(resp.content)
88
+
89
+ instance = random.choice(service.hosts)
90
+
91
+ http_client = httpx2.Client(base_url=f"http://{instance.ip}:{instance.port}/{service_name}", headers=headers)
92
+
93
+ # 主进程结束前执行 http_client.close()
94
+ atexit.register(http_client.close)
95
+
96
+ return http_client