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,485 @@
|
|
|
1
|
+
"""
|
|
2
|
+
基于 SQLAlchemy 的同步数据库连接实现。
|
|
3
|
+
|
|
4
|
+
继承自 DatabaseConnBase,提供统一的数据库连接接口。
|
|
5
|
+
使用 SQLAlchemy Engine 和 Session 作为底层实现,支持 MySQL、PostgreSQL、DM、Oracle 等多种数据库。
|
|
6
|
+
|
|
7
|
+
Classes:
|
|
8
|
+
DatabaseConn: 基于 SQLAlchemy 的同步数据库连接类
|
|
9
|
+
|
|
10
|
+
Example:
|
|
11
|
+
>>> with DatabaseConn(task_id="t1", host="localhost", port=3306,
|
|
12
|
+
... user="root", password="pass", db="test") as conn:
|
|
13
|
+
... result = conn.select("SELECT * FROM users WHERE id = %s", (1,))
|
|
14
|
+
... conn.execute("INSERT INTO users (name) VALUES (%s)", ("Alice",))
|
|
15
|
+
... conn.executemany("INSERT INTO users (name) VALUES (%s)", [("Bob",), ("Carol",)])
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import re
|
|
19
|
+
from collections.abc import Mapping
|
|
20
|
+
from typing import Any, Final, Literal, overload
|
|
21
|
+
|
|
22
|
+
from sqlalchemy import URL, Engine, create_engine, text
|
|
23
|
+
from sqlalchemy.orm import Session, sessionmaker
|
|
24
|
+
|
|
25
|
+
from .dbConn_base import DatabaseConnBase
|
|
26
|
+
|
|
27
|
+
# 数据库类型到驱动程序的映射
|
|
28
|
+
# 用于 build_url 函数构建数据库连接 URL
|
|
29
|
+
DBTYPE_DRIVER: Final[Mapping[str, str]] = {
|
|
30
|
+
"mysql": "pymysql",
|
|
31
|
+
"postgresql": "psycopg",
|
|
32
|
+
"dm": "dmpython",
|
|
33
|
+
"oracle": "oracledb",
|
|
34
|
+
"doris": "pymysql",
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def build_url(
|
|
39
|
+
host: str,
|
|
40
|
+
port: int | str,
|
|
41
|
+
user: str,
|
|
42
|
+
password: str,
|
|
43
|
+
db: str,
|
|
44
|
+
db_type: str,
|
|
45
|
+
) -> str | URL:
|
|
46
|
+
"""
|
|
47
|
+
构建数据库连接 URL 字符串。
|
|
48
|
+
|
|
49
|
+
将数据库连接参数格式化为 SQLAlchemy 所使用的 URL 格式。
|
|
50
|
+
用户名和密码会进行 URL 编码,以处理特殊字符。
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
host: 数据库服务器主机地址。
|
|
54
|
+
port: 数据库服务器端口号。
|
|
55
|
+
user: 数据库用户名。
|
|
56
|
+
password: 数据库密码。
|
|
57
|
+
db: 数据库名称。
|
|
58
|
+
db_type: 数据库类型
|
|
59
|
+
|
|
60
|
+
Returns:
|
|
61
|
+
格式化的数据库连接 URL 字符串。
|
|
62
|
+
|
|
63
|
+
Raises:
|
|
64
|
+
NotImplementedError: 当指定的 db_type 不在支持列表中时抛出。
|
|
65
|
+
|
|
66
|
+
Example:
|
|
67
|
+
>>> url = build_url("localhost", 3306, "root", "password", "mydb", "mysql")
|
|
68
|
+
>>> print(url)
|
|
69
|
+
mysql+pymysql://root:password@localhost:3306/mydb
|
|
70
|
+
"""
|
|
71
|
+
# 检查数据库类型是否支持
|
|
72
|
+
if db_type not in DBTYPE_DRIVER:
|
|
73
|
+
raise NotImplementedError(f"{db_type} 尚未支持,请联系管理员")
|
|
74
|
+
|
|
75
|
+
query: dict[str, str] = dict()
|
|
76
|
+
if db_type in {"mysql", "doris"}:
|
|
77
|
+
query["local_infile"] = "True"
|
|
78
|
+
query["ssl_disabled"] = "True"
|
|
79
|
+
|
|
80
|
+
driver = DBTYPE_DRIVER[db_type]
|
|
81
|
+
drivername = f"{db_type}+{driver}" if driver else db_type
|
|
82
|
+
return URL.create(
|
|
83
|
+
drivername=drivername,
|
|
84
|
+
username=user,
|
|
85
|
+
password=password,
|
|
86
|
+
host=host,
|
|
87
|
+
port=int(port),
|
|
88
|
+
database=db,
|
|
89
|
+
query=query,
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
class DatabaseConn(DatabaseConnBase):
|
|
94
|
+
"""
|
|
95
|
+
基于 SQLAlchemy 的同步数据库连接类。
|
|
96
|
+
|
|
97
|
+
使用 SQLAlchemy Engine 和 Session 实现数据库操作,支持:
|
|
98
|
+
- 单条 SQL 执行(execute)
|
|
99
|
+
- 批量 SQL 执行(executemany)
|
|
100
|
+
- 查询操作(select)
|
|
101
|
+
- 上下文管理器(自动关闭连接)
|
|
102
|
+
- 事务管理(commit/rollback)
|
|
103
|
+
|
|
104
|
+
继承自 DatabaseConnBase,实现所有抽象方法。
|
|
105
|
+
|
|
106
|
+
Attributes:
|
|
107
|
+
engine: SQLAlchemy Engine 对象。
|
|
108
|
+
session: SQLAlchemy Session 对象。
|
|
109
|
+
logger: 日志记录器。
|
|
110
|
+
|
|
111
|
+
Example:
|
|
112
|
+
>>> with DatabaseConn(task_id="t1", host="localhost", port=3306,
|
|
113
|
+
... user="root", password="pass", db="test",
|
|
114
|
+
... db_type="mysql") as conn:
|
|
115
|
+
... # 查询数据
|
|
116
|
+
... rows = conn.select("SELECT * FROM users LIMIT %s", (10,))
|
|
117
|
+
... # 插入数据
|
|
118
|
+
... conn.execute("INSERT INTO users (name, email) VALUES (%s, %s)",
|
|
119
|
+
... ("Alice", "alice@example.com"))
|
|
120
|
+
... # 批量插入
|
|
121
|
+
... conn.executemany("INSERT INTO users (name) VALUES (%s)",
|
|
122
|
+
... [("Bob",), ("Carol",)])
|
|
123
|
+
"""
|
|
124
|
+
|
|
125
|
+
def post_init(self):
|
|
126
|
+
"""
|
|
127
|
+
初始化 SQLAlchemy Engine 和 Session。
|
|
128
|
+
|
|
129
|
+
使用 build_url 构建数据库 URL,创建 Engine 和 SessionFactory。
|
|
130
|
+
|
|
131
|
+
Returns:
|
|
132
|
+
Session: SQLAlchemy Session 对象。
|
|
133
|
+
"""
|
|
134
|
+
# 构建数据库连接 URL
|
|
135
|
+
self._url = build_url(
|
|
136
|
+
host=self._host,
|
|
137
|
+
port=self._port,
|
|
138
|
+
user=self._user,
|
|
139
|
+
password=self._password,
|
|
140
|
+
db=self._db,
|
|
141
|
+
db_type=self._db_type,
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
# 创建 SQLAlchemy Engine
|
|
145
|
+
if self._log_sql:
|
|
146
|
+
self._logger.info(
|
|
147
|
+
f"Creating SQLAlchemy Engine for {self._host}:{self._port}/{self._db}"
|
|
148
|
+
)
|
|
149
|
+
self._engine: Engine = create_engine_safe(self._db_type, self._url)
|
|
150
|
+
|
|
151
|
+
# 创建 SessionFactory
|
|
152
|
+
self._session_factory = sessionmaker(
|
|
153
|
+
bind=self._engine,
|
|
154
|
+
autoflush=False,
|
|
155
|
+
expire_on_commit=False,
|
|
156
|
+
autocommit=False,
|
|
157
|
+
)
|
|
158
|
+
self._session = None
|
|
159
|
+
|
|
160
|
+
def _get_log_sql(self, log_sql: bool | None) -> bool:
|
|
161
|
+
"""
|
|
162
|
+
获取是否记录 SQL 的布尔值。
|
|
163
|
+
|
|
164
|
+
优先级:方法参数 log_sql > 实例级 self._log_sql
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
log_sql: 方法级 log_sql 参数。
|
|
168
|
+
|
|
169
|
+
Returns:
|
|
170
|
+
bool: 是否应记录 SQL。
|
|
171
|
+
"""
|
|
172
|
+
return bool(log_sql) if log_sql is not None else self._log_sql
|
|
173
|
+
|
|
174
|
+
@staticmethod
|
|
175
|
+
def render_sql(sql: str, args: tuple | list | dict | None) -> str:
|
|
176
|
+
"""
|
|
177
|
+
渲染 SQL 语句。
|
|
178
|
+
|
|
179
|
+
将 SQL 语句和参数组合成完整的 SQL 语句。
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
sql: SQL 语句模板。
|
|
183
|
+
args: SQL 语句参数。
|
|
184
|
+
|
|
185
|
+
Returns:
|
|
186
|
+
str: 渲染后的 SQL 语句。
|
|
187
|
+
"""
|
|
188
|
+
if not args:
|
|
189
|
+
return sql
|
|
190
|
+
if isinstance(args, (list, tuple)):
|
|
191
|
+
return sql.replace("%s", "{}").format(*args)
|
|
192
|
+
else:
|
|
193
|
+
# 把 :name 替换为 {name}
|
|
194
|
+
sql = re.sub(r"\%\((\w+)\)s", r"\{\1\}", sql)
|
|
195
|
+
return sql.format(**args)
|
|
196
|
+
|
|
197
|
+
def executemany(
|
|
198
|
+
self,
|
|
199
|
+
sql: str,
|
|
200
|
+
datas: list[tuple | list],
|
|
201
|
+
*,
|
|
202
|
+
auto_commit: bool = True,
|
|
203
|
+
log_sql: bool | None = None,
|
|
204
|
+
**kwargs,
|
|
205
|
+
):
|
|
206
|
+
"""
|
|
207
|
+
执行多条 SQL 语句(批量执行)。
|
|
208
|
+
|
|
209
|
+
用于批量插入、更新等场景。会自动提交事务。
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
sql: 待执行的 SQL 语句模板,支持 %s 或 :name 占位符。
|
|
213
|
+
datas: SQL 语句参数的列表,每一项对应一条 SQL 执行。
|
|
214
|
+
log_sql: 是否打印 SQL 日志,为 None 时使用实例级 self._log_sql。
|
|
215
|
+
"""
|
|
216
|
+
if kwargs:
|
|
217
|
+
self.logger.warning(f"尚未支持的参数: {kwargs}")
|
|
218
|
+
|
|
219
|
+
should_log = self._get_log_sql(log_sql)
|
|
220
|
+
|
|
221
|
+
sql, args = self.process_input(sql, args)
|
|
222
|
+
|
|
223
|
+
if should_log:
|
|
224
|
+
self.logger.info(self.render_sql(sql, args[0]))
|
|
225
|
+
|
|
226
|
+
if self.session:
|
|
227
|
+
self.session.execute(text(sql), datas)
|
|
228
|
+
|
|
229
|
+
if auto_commit:
|
|
230
|
+
session.commit()
|
|
231
|
+
else:
|
|
232
|
+
with self.session_factory() as session:
|
|
233
|
+
session.execute(text(sql), datas)
|
|
234
|
+
|
|
235
|
+
if auto_commit:
|
|
236
|
+
session.commit()
|
|
237
|
+
|
|
238
|
+
def execute(
|
|
239
|
+
self,
|
|
240
|
+
sql: str,
|
|
241
|
+
args: tuple | list | dict | None = None,
|
|
242
|
+
*,
|
|
243
|
+
auto_commit: bool = True,
|
|
244
|
+
log_sql: bool | None = None,
|
|
245
|
+
**kwargs,
|
|
246
|
+
):
|
|
247
|
+
"""
|
|
248
|
+
执行单条 SQL 语句。
|
|
249
|
+
|
|
250
|
+
可执行 INSERT、UPDATE、DELETE、CREATE 等语句。
|
|
251
|
+
默认自动提交事务。
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
sql: 待执行的 SQL 语句。
|
|
255
|
+
args: SQL 语句参数,支持元组、列表或字典格式。
|
|
256
|
+
log_sql: 是否打印 SQL 日志,为 None 时使用实例级 self._log_sql。
|
|
257
|
+
"""
|
|
258
|
+
if kwargs:
|
|
259
|
+
self.logger.warning(f"尚未支持的参数: {kwargs}")
|
|
260
|
+
|
|
261
|
+
should_log = self._get_log_sql(log_sql)
|
|
262
|
+
|
|
263
|
+
sql, args = self.process_input(sql, args)
|
|
264
|
+
|
|
265
|
+
if should_log:
|
|
266
|
+
self.logger.info(self.render_sql(sql, args))
|
|
267
|
+
|
|
268
|
+
if self.session:
|
|
269
|
+
self.session.execute(text(sql), args)
|
|
270
|
+
|
|
271
|
+
if auto_commit:
|
|
272
|
+
session.commit()
|
|
273
|
+
else:
|
|
274
|
+
with self.session_factory() as session:
|
|
275
|
+
session.execute(text(sql), args)
|
|
276
|
+
|
|
277
|
+
if auto_commit:
|
|
278
|
+
session.commit()
|
|
279
|
+
|
|
280
|
+
@overload
|
|
281
|
+
def select(
|
|
282
|
+
self,
|
|
283
|
+
sql: str,
|
|
284
|
+
args: tuple | list | dict | None = None,
|
|
285
|
+
size: int | None = None,
|
|
286
|
+
mapping: Literal[True] = True,
|
|
287
|
+
*,
|
|
288
|
+
log_sql: bool | None = None,
|
|
289
|
+
) -> list[dict[str, Any]]:
|
|
290
|
+
...
|
|
291
|
+
|
|
292
|
+
@overload
|
|
293
|
+
def select(
|
|
294
|
+
self,
|
|
295
|
+
sql: str,
|
|
296
|
+
args: tuple | list | dict | None = None,
|
|
297
|
+
size: int | None = None,
|
|
298
|
+
mapping: Literal[False] = False,
|
|
299
|
+
*,
|
|
300
|
+
log_sql: bool | None = None,
|
|
301
|
+
) -> list[tuple[Any]]:
|
|
302
|
+
...
|
|
303
|
+
|
|
304
|
+
def select(
|
|
305
|
+
self,
|
|
306
|
+
sql: str,
|
|
307
|
+
args: tuple | list | dict | None = None,
|
|
308
|
+
size: int | None = None,
|
|
309
|
+
mapping: bool = False,
|
|
310
|
+
*,
|
|
311
|
+
log_sql: bool | None = None,
|
|
312
|
+
) -> list[dict[str, Any] | tuple[Any]]:
|
|
313
|
+
"""
|
|
314
|
+
执行查询语句并返回结果。
|
|
315
|
+
|
|
316
|
+
用于 SELECT 查询场景,返回查询到的数据行列表。
|
|
317
|
+
|
|
318
|
+
Args:
|
|
319
|
+
sql: 待执行的查询 SQL 语句。
|
|
320
|
+
args: SQL 语句参数,支持元组、列表或字典格式。
|
|
321
|
+
size: 限制返回的行数,None 表示不限制。
|
|
322
|
+
log_sql: 是否打印 SQL 日志,为 None 时使用实例级 self._log_sql。
|
|
323
|
+
|
|
324
|
+
Returns:
|
|
325
|
+
list[tuple]: 查询结果,元素为 Row 对象或字典(取决于 SQLAlchemy 配置)。
|
|
326
|
+
"""
|
|
327
|
+
should_log = self._get_log_sql(log_sql)
|
|
328
|
+
|
|
329
|
+
if isinstance(sql, str):
|
|
330
|
+
sql, args = self.process_input(sql, args)
|
|
331
|
+
|
|
332
|
+
if should_log:
|
|
333
|
+
self.logger.info(self.render_sql(sql, args))
|
|
334
|
+
|
|
335
|
+
with self.session_factory() as session, session.connection() as conn:
|
|
336
|
+
result = (
|
|
337
|
+
conn.exec_driver_sql(sql, args)
|
|
338
|
+
if isinstance(sql, str)
|
|
339
|
+
else sql.execute(session.connection())
|
|
340
|
+
)
|
|
341
|
+
|
|
342
|
+
if mapping:
|
|
343
|
+
result = result.mappings()
|
|
344
|
+
# 获取结果
|
|
345
|
+
rows = result.fetchall() if size is None or size <= 0 else result.fetchmany(size)
|
|
346
|
+
|
|
347
|
+
return list(map(dict, rows)) if mapping else list(map(lambda x: x._t._data, rows))
|
|
348
|
+
|
|
349
|
+
@staticmethod
|
|
350
|
+
def process_input(sql: str, args: dict | tuple | list | None):
|
|
351
|
+
# sql = sql.replace("%s", "?")
|
|
352
|
+
# # 把 %(key)s 替换为 :key
|
|
353
|
+
# sql = re.sub(r"\%\((\w+)\)s", r":\1", sql)
|
|
354
|
+
if args is None:
|
|
355
|
+
return sql, None
|
|
356
|
+
if isinstance(args, list):
|
|
357
|
+
if len(args) > 1 and isinstance(args[0], list):
|
|
358
|
+
return sql, list(map(tuple, args))
|
|
359
|
+
else:
|
|
360
|
+
return sql, tuple(args)
|
|
361
|
+
# return sql, args
|
|
362
|
+
else:
|
|
363
|
+
return sql, args
|
|
364
|
+
|
|
365
|
+
def close(self):
|
|
366
|
+
"""
|
|
367
|
+
关闭数据库连接。
|
|
368
|
+
|
|
369
|
+
关闭 Session 和 Engine,释放数据库连接资源。
|
|
370
|
+
支持多次调用,子类应实现幂等性。
|
|
371
|
+
"""
|
|
372
|
+
# 关闭 Session
|
|
373
|
+
if hasattr(self, "_session") and self._session is not None:
|
|
374
|
+
self._session.close()
|
|
375
|
+
self._session = None
|
|
376
|
+
|
|
377
|
+
# 关闭 Engine(会关闭所有连接池)
|
|
378
|
+
if hasattr(self, "_engine") and self._engine is not None:
|
|
379
|
+
self.engine.dispose()
|
|
380
|
+
self._engine = None
|
|
381
|
+
self._logger.info(f"closed")
|
|
382
|
+
|
|
383
|
+
@property
|
|
384
|
+
def session_factory(self):
|
|
385
|
+
return self._session_factory
|
|
386
|
+
|
|
387
|
+
@property
|
|
388
|
+
def engine(self) -> Engine:
|
|
389
|
+
"""
|
|
390
|
+
获取 SQLAlchemy Engine 对象。
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
Engine: SQLAlchemy Engine 对象。
|
|
394
|
+
"""
|
|
395
|
+
return self._engine
|
|
396
|
+
|
|
397
|
+
def commit(self):
|
|
398
|
+
"""
|
|
399
|
+
提交当前事务。
|
|
400
|
+
"""
|
|
401
|
+
self._logger.debug(f"committing")
|
|
402
|
+
self.session.commit()
|
|
403
|
+
|
|
404
|
+
def rollback(self):
|
|
405
|
+
"""
|
|
406
|
+
回滚当前事务。
|
|
407
|
+
"""
|
|
408
|
+
self._logger.warning(f"rolling back")
|
|
409
|
+
self.session.rollback()
|
|
410
|
+
|
|
411
|
+
def begin(self):
|
|
412
|
+
"""
|
|
413
|
+
开始一个新的事务。
|
|
414
|
+
"""
|
|
415
|
+
self._logger.debug(f"beginning")
|
|
416
|
+
if self.session:
|
|
417
|
+
assert self._session is not None, "session not closed"
|
|
418
|
+
self._session = self.session_factory()
|
|
419
|
+
self._session.begin()
|
|
420
|
+
|
|
421
|
+
@property
|
|
422
|
+
def session(self) -> Session | None:
|
|
423
|
+
"""
|
|
424
|
+
获取当前事务的 Session 对象。
|
|
425
|
+
|
|
426
|
+
Returns:
|
|
427
|
+
Session: 当前事务的 Session 对象。
|
|
428
|
+
"""
|
|
429
|
+
return self._session
|
|
430
|
+
|
|
431
|
+
class DatabaseConnDataOps(DatabaseConn):
|
|
432
|
+
def __init__(self, datasource: str, log_sql: bool | None = None):
|
|
433
|
+
from dataops_sdk.services.meta import Meta, release_list_get_by_name
|
|
434
|
+
|
|
435
|
+
datasource_meta = Meta.model_validate(release_list_get_by_name("datasource", datasource))
|
|
436
|
+
|
|
437
|
+
super().__init__(
|
|
438
|
+
task_id="",
|
|
439
|
+
host=datasource_meta.data["ip"],
|
|
440
|
+
port=datasource_meta.data["port"],
|
|
441
|
+
user=datasource_meta.data["username"],
|
|
442
|
+
password=datasource_meta.data["password"],
|
|
443
|
+
db=datasource_meta.data["database"],
|
|
444
|
+
db_type=datasource_meta.data["type"],
|
|
445
|
+
log_sql=log_sql,
|
|
446
|
+
)
|
|
447
|
+
|
|
448
|
+
|
|
449
|
+
def create_engine_safe(db_type: str, url: str, **kwargs: Any) -> Engine:
|
|
450
|
+
"""
|
|
451
|
+
安全创建 SQLAlchemy Engine。
|
|
452
|
+
|
|
453
|
+
处理各种数据库驱动的兼容性问题。
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
url: 数据库连接 URL。
|
|
457
|
+
**kwargs: 传递给 create_engine 的参数。
|
|
458
|
+
|
|
459
|
+
Returns:
|
|
460
|
+
Engine: SQLAlchemy Engine 对象。
|
|
461
|
+
|
|
462
|
+
Raises:
|
|
463
|
+
NotImplementedError: 当数据库类型不支持时抛出。
|
|
464
|
+
"""
|
|
465
|
+
global DBTYPE_DRIVER
|
|
466
|
+
|
|
467
|
+
if db_type not in DBTYPE_DRIVER:
|
|
468
|
+
raise NotImplementedError(f"{db_type} 尚未支持,请联系管理员")
|
|
469
|
+
|
|
470
|
+
# 根据不同数据库类型调整连接参数
|
|
471
|
+
if db_type in ("mysql", "doris"):
|
|
472
|
+
# MySQL 需要添加一些兼容参数
|
|
473
|
+
kwargs.setdefault("pool_pre_ping", True)
|
|
474
|
+
kwargs.setdefault("pool_recycle", 3600)
|
|
475
|
+
elif db_type == "postgresql":
|
|
476
|
+
# PostgreSQL 配置
|
|
477
|
+
kwargs.setdefault("pool_pre_ping", True)
|
|
478
|
+
elif db_type == "dm":
|
|
479
|
+
# DM 数据库配置
|
|
480
|
+
kwargs.setdefault("pool_pre_ping", True)
|
|
481
|
+
elif db_type == "oracle":
|
|
482
|
+
# Oracle 配置
|
|
483
|
+
kwargs.setdefault("pool_pre_ping", True)
|
|
484
|
+
|
|
485
|
+
return create_engine(url, **kwargs)
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"""DuckDB 管理模块。
|
|
2
|
+
|
|
3
|
+
提供 DuckDB 连接和生命周期管理,支持从传统数据库导入数据到 DuckDB。
|
|
4
|
+
|
|
5
|
+
Classes:
|
|
6
|
+
DuckDBManager: DuckDB 连接和生命周期管理器
|
|
7
|
+
|
|
8
|
+
Example:
|
|
9
|
+
>>> with DuckDBManager('task1') as conn:
|
|
10
|
+
... conn.sql("SELECT * FROM table").fetchall()
|
|
11
|
+
# 自动关闭连接并删除临时文件
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import asyncio
|
|
15
|
+
from collections.abc import Coroutine
|
|
16
|
+
from pathlib import Path
|
|
17
|
+
from typing import Any
|
|
18
|
+
from uuid import uuid4
|
|
19
|
+
|
|
20
|
+
import duckdb
|
|
21
|
+
import pandas as pd
|
|
22
|
+
|
|
23
|
+
from .dbConn_sync import DatabaseConn
|
|
24
|
+
from .log import getLogger
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class DuckDBManager:
|
|
28
|
+
"""
|
|
29
|
+
DuckDB 连接和生命周期管理器
|
|
30
|
+
|
|
31
|
+
支持上下文管理器(with语法),自动管理连接和临时文件清理
|
|
32
|
+
|
|
33
|
+
使用示例:
|
|
34
|
+
with DuckDBManager('port') as conn:
|
|
35
|
+
conn.sql("SELECT * FROM table").fetchall()
|
|
36
|
+
# 自动关闭连接并删除临时文件
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, task_id: str, prefix: str = "duckdb"):
|
|
40
|
+
"""
|
|
41
|
+
初始化 DuckDB 管理器
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
prefix: 临时文件前缀,默认 'duckdb'
|
|
45
|
+
"""
|
|
46
|
+
self._conn: duckdb.DuckDBPyConnection | None = None
|
|
47
|
+
self._temp_file = Path(f"{prefix}_{uuid4().hex}.db")
|
|
48
|
+
self._logger = getLogger(task_id)
|
|
49
|
+
self._loop = None
|
|
50
|
+
|
|
51
|
+
def _run_coroutine(self, coro: Coroutine):
|
|
52
|
+
"""同步执行异步协程"""
|
|
53
|
+
if self._loop is None:
|
|
54
|
+
loop = asyncio.get_event_loop()
|
|
55
|
+
if loop is not None:
|
|
56
|
+
self._loop = loop
|
|
57
|
+
else:
|
|
58
|
+
self._loop = asyncio.new_event_loop()
|
|
59
|
+
assert self._loop, "no running loop"
|
|
60
|
+
|
|
61
|
+
return self._loop.run_until_complete(coro)
|
|
62
|
+
|
|
63
|
+
def __enter__(self) -> duckdb.DuckDBPyConnection:
|
|
64
|
+
"""进入上下文,创建连接"""
|
|
65
|
+
self._conn = duckdb.connect(self._temp_file)
|
|
66
|
+
self._logger.info(f"DuckDB connection created: {self._temp_file}")
|
|
67
|
+
return self._conn
|
|
68
|
+
|
|
69
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
70
|
+
"""退出上下文,关闭连接并清理临时文件"""
|
|
71
|
+
self.close()
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def conn(self) -> duckdb.DuckDBPyConnection | None:
|
|
76
|
+
"""获取连接对象"""
|
|
77
|
+
return self._conn
|
|
78
|
+
|
|
79
|
+
@property
|
|
80
|
+
def is_connected(self) -> bool:
|
|
81
|
+
"""检查是否已连接"""
|
|
82
|
+
return self._conn is not None
|
|
83
|
+
|
|
84
|
+
def execute(self, sql: str, parameters: object = None) -> duckdb.DuckDBPyConnection:
|
|
85
|
+
"""执行 SQL 语句"""
|
|
86
|
+
self.open()
|
|
87
|
+
return self._conn.execute(sql, parameters=parameters)
|
|
88
|
+
|
|
89
|
+
def sql(
|
|
90
|
+
self, sql: duckdb.Statement | str, *, alias: str = "", params: object = None
|
|
91
|
+
) -> duckdb.DuckDBPyConnection:
|
|
92
|
+
"""执行 SQL 查询并返回结果"""
|
|
93
|
+
self.open()
|
|
94
|
+
return self._conn.sql(sql, alias=alias, params=params)
|
|
95
|
+
|
|
96
|
+
def close(self):
|
|
97
|
+
"""关闭连接并清理临时文件"""
|
|
98
|
+
if self._conn:
|
|
99
|
+
try:
|
|
100
|
+
self._conn.close()
|
|
101
|
+
self._logger.info("DuckDB connection closed")
|
|
102
|
+
except Exception as e:
|
|
103
|
+
self._logger.warning(f"Error closing DuckDB connection: {e}")
|
|
104
|
+
self._conn = None
|
|
105
|
+
|
|
106
|
+
if self._temp_file and self._temp_file.exists():
|
|
107
|
+
try:
|
|
108
|
+
self._temp_file.unlink()
|
|
109
|
+
self._logger.info(f"Temporary file {self._temp_file} deleted")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
self._logger.warning(f"Error deleting temporary file {self._temp_file}: {e}")
|
|
112
|
+
|
|
113
|
+
def open(self) -> duckdb.DuckDBPyConnection:
|
|
114
|
+
"""打开连接(推荐使用 with 语句)"""
|
|
115
|
+
if not self._conn:
|
|
116
|
+
self._conn = duckdb.connect(self._temp_file)
|
|
117
|
+
self._logger.info(f"DuckDB connection created: {self._temp_file}")
|
|
118
|
+
return self._conn
|
|
119
|
+
|
|
120
|
+
def import_from_db(
|
|
121
|
+
self,
|
|
122
|
+
db_conn: DatabaseConn,
|
|
123
|
+
sql: str,
|
|
124
|
+
table_name: str,
|
|
125
|
+
page_size: int = 200000,
|
|
126
|
+
):
|
|
127
|
+
"""
|
|
128
|
+
从数据库导入数据到 duckdb
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
duck_db_conn: duckdb 数据库连接
|
|
132
|
+
db_conn: 数据库连接
|
|
133
|
+
sql: SQL 语句
|
|
134
|
+
table_name: duckdb 表名
|
|
135
|
+
page_size: 分页大小
|
|
136
|
+
"""
|
|
137
|
+
self.open()
|
|
138
|
+
page_num = 1
|
|
139
|
+
total_count = 0
|
|
140
|
+
db_conn.execute(sql)
|
|
141
|
+
|
|
142
|
+
# 从cursor.description获取列名
|
|
143
|
+
columns = [col[0] for col in db_conn.cursor.description]
|
|
144
|
+
|
|
145
|
+
if db_conn.cursor.rowcount == 0:
|
|
146
|
+
# 如果没有数据,需要在 duckdb 创建表结构
|
|
147
|
+
db_conn.logger.info("No data from query, creating table structure in DuckDB")
|
|
148
|
+
field_str = ", ".join([f"{col} VARCHAR" for col in columns])
|
|
149
|
+
sql_create_table = f"CREATE TABLE IF NOT EXISTS {table_name} ({field_str})"
|
|
150
|
+
db_conn.logger.info(f"Create table SQL: {sql_create_table}")
|
|
151
|
+
# await asyncio.to_thread(duck_db_conn.execute, sql_create_table)
|
|
152
|
+
self.execute(sql_create_table)
|
|
153
|
+
else:
|
|
154
|
+
while True:
|
|
155
|
+
db_conn.logger.info(f"Processing page {page_num}")
|
|
156
|
+
result_list = db_conn.cursor.fetchmany(page_size)
|
|
157
|
+
df = pd.DataFrame(result_list, columns=columns)
|
|
158
|
+
if len(df) == 0:
|
|
159
|
+
break
|
|
160
|
+
if page_num == 1:
|
|
161
|
+
# 第一次导入数据时,先创建表结构
|
|
162
|
+
# 将 DataFrame 注册为临时视图,以便创建表结构
|
|
163
|
+
self._conn.register("df_view", df)
|
|
164
|
+
self.execute(
|
|
165
|
+
f"CREATE TABLE IF NOT EXISTS {table_name} AS SELECT * FROM df_view WHERE 1=0"
|
|
166
|
+
)
|
|
167
|
+
self._conn.unregister("df_view")
|
|
168
|
+
page_num += 1
|
|
169
|
+
# 导入数据到 duckdb
|
|
170
|
+
self._conn.append(table_name, df)
|
|
171
|
+
total_count += len(df)
|
|
172
|
+
db_conn.logger.info(
|
|
173
|
+
f"Page {page_num}: imported {total_count} rows to DuckDB table {table_name}"
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
db_conn.logger.info(
|
|
177
|
+
f"Import completed: {total_count} rows imported to DuckDB table {table_name}"
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
@staticmethod
|
|
181
|
+
def to_list(result_list: list, description: list, default_value: Any = None) -> list[dict]:
|
|
182
|
+
"""
|
|
183
|
+
将查询结果转换为字典列表
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
result_list: 查询结果
|
|
187
|
+
description: 列描述
|
|
188
|
+
default_value: 默认值
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
字典列表
|
|
192
|
+
"""
|
|
193
|
+
columns = [col[0] for col in description]
|
|
194
|
+
res = []
|
|
195
|
+
for data_item in result_list:
|
|
196
|
+
res_item = {}
|
|
197
|
+
for i, col_name in enumerate(columns):
|
|
198
|
+
res_item[col_name] = data_item[i] if data_item[i] is not None else default_value
|
|
199
|
+
res.append(res_item)
|
|
200
|
+
return res
|
|
201
|
+
|
|
202
|
+
def __del__(self):
|
|
203
|
+
self.close()
|