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.
- dataops_sdk/__init__.py +84 -0
- dataops_sdk/constants.py +6 -0
- dataops_sdk/exceptions.py +31 -0
- dataops_sdk/runner/__init__.py +46 -0
- dataops_sdk/runner/datasource_factory.py +83 -0
- dataops_sdk/runner/schemas/nacos/__init__.py +5 -0
- dataops_sdk/runner/schemas/nacos/config.py +40 -0
- dataops_sdk/runner/schemas/nacos/constants.py +235 -0
- dataops_sdk/runner/schemas/nacos/instance.py +103 -0
- dataops_sdk/runner/schemas/nacos/nacos_exception.py +44 -0
- dataops_sdk/runner/schemas/nacos/naming_param.py +62 -0
- dataops_sdk/runner/schemas/nacos/naming_request.py +84 -0
- dataops_sdk/runner/schemas/nacos/naming_response.py +50 -0
- dataops_sdk/runner/schemas/nacos/preserved_metadata_key.py +10 -0
- dataops_sdk/runner/schemas/nacos/service.py +119 -0
- dataops_sdk/runner/utils/__init__.py +42 -0
- dataops_sdk/runner/utils/common.py +209 -0
- dataops_sdk/runner/utils/dbConn_base.py +256 -0
- dataops_sdk/runner/utils/dbConn_sync.py +485 -0
- dataops_sdk/runner/utils/duckdb_manager.py +203 -0
- dataops_sdk/runner/utils/file_transfer_base.py +307 -0
- dataops_sdk/runner/utils/ftp_file_transfer.py +242 -0
- dataops_sdk/runner/utils/log.py +50 -0
- dataops_sdk/runner/utils/nacos.py +96 -0
- dataops_sdk/runner/utils/neo4j_sync.py +236 -0
- dataops_sdk/runner/utils/queue_base.py +224 -0
- dataops_sdk/runner/utils/queue_kafka.py +150 -0
- dataops_sdk/runner/utils/sftp_file_transfer.py +243 -0
- dataops_sdk/schemas/__init__.py +32 -0
- dataops_sdk/schemas/alarm.py +38 -0
- dataops_sdk/schemas/api.py +37 -0
- dataops_sdk/schemas/data_table.py +437 -0
- dataops_sdk/schemas/datasource.py +26 -0
- dataops_sdk/schemas/dict_.py +13 -0
- dataops_sdk/schemas/enums.py +120 -0
- dataops_sdk/schemas/group.py +40 -0
- dataops_sdk/schemas/hazard.py +151 -0
- dataops_sdk/schemas/meta.py +175 -0
- dataops_sdk/schemas/query.py +175 -0
- dataops_sdk/schemas/report.py +22 -0
- dataops_sdk/schemas/skill.py +70 -0
- dataops_sdk/schemas/table.py +65 -0
- dataops_sdk/schemas/user.py +52 -0
- dataops_sdk/schemas/widget.py +651 -0
- dataops_sdk/schemas/workflow.py +63 -0
- dataops_sdk/services/__init__.py +110 -0
- dataops_sdk/services/alarm.py +324 -0
- dataops_sdk/services/api.py +28 -0
- dataops_sdk/services/auth.py +120 -0
- dataops_sdk/services/data_table.py +485 -0
- dataops_sdk/services/dataset.py +127 -0
- dataops_sdk/services/datasource.py +57 -0
- dataops_sdk/services/dict_.py +69 -0
- dataops_sdk/services/hazard.py +260 -0
- dataops_sdk/services/meta.py +523 -0
- dataops_sdk/services/metrics.py +42 -0
- dataops_sdk/services/ref.py +66 -0
- dataops_sdk/services/report.py +36 -0
- dataops_sdk/services/skill.py +276 -0
- dataops_sdk/services/table.py +340 -0
- dataops_sdk/services/widget.py +143 -0
- dataops_sdk/services/workflow.py +102 -0
- dataops_sdk/session.py +292 -0
- dataops_sdk/tiptap/__init__.py +30 -0
- dataops_sdk/tiptap/builder.py +75 -0
- dataops_sdk/tiptap/constants.py +133 -0
- dataops_sdk/tiptap/markdown_parser.py +294 -0
- dataops_sdk/tiptap/models.py +43 -0
- dataops_sdk/tiptap/parse_utils.py +889 -0
- dataops_sdk/tiptap/render_utils.py +846 -0
- dataops_sdk/tiptap/renderers.py +51 -0
- dataops_sdk/tiptap/tool.py +629 -0
- dataops_sdk/utils/__init__.py +15 -0
- dataops_sdk/utils/common.py +211 -0
- dataops_sdk/utils/utils.py +120 -0
- dataops_sdk-0.3.0.dist-info/METADATA +25 -0
- dataops_sdk-0.3.0.dist-info/RECORD +79 -0
- dataops_sdk-0.3.0.dist-info/WHEEL +5 -0
- 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
|