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,276 @@
|
|
|
1
|
+
from collections.abc import Mapping
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
from pydantic_core import to_json
|
|
5
|
+
|
|
6
|
+
from dataops_sdk.exceptions import AuthenticationError, DataOpsError
|
|
7
|
+
from dataops_sdk.schemas import DataSkill, MetaRelease, MetaSkill, MetaType, SkillCallResult
|
|
8
|
+
from dataops_sdk.services.meta import (
|
|
9
|
+
release_list_get_by_code,
|
|
10
|
+
release_list_get_by_name,
|
|
11
|
+
release_list_list,
|
|
12
|
+
)
|
|
13
|
+
from dataops_sdk.session import get_session, post_multipart
|
|
14
|
+
from dataops_sdk.utils import encrypt_body, filter_nodes_by_pattern
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"service_call_skill_by_code",
|
|
18
|
+
"service_delete_skill",
|
|
19
|
+
"service_delete_sub_file",
|
|
20
|
+
"service_get_skill_by_code",
|
|
21
|
+
"service_get_skill_by_name",
|
|
22
|
+
"service_import_skill",
|
|
23
|
+
"service_list_skill_files",
|
|
24
|
+
"service_list_skills",
|
|
25
|
+
"service_upload_sub_file",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def service_list_skills(query: str | None = None) -> list[MetaRelease]:
|
|
30
|
+
"""列出技能(可选正则过滤).
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
query: 正则表达式,匹配 NAME / TITLE / REMARK / DIGEST.
|
|
34
|
+
|
|
35
|
+
Returns:
|
|
36
|
+
MetaRelease 对象列表
|
|
37
|
+
|
|
38
|
+
Raises:
|
|
39
|
+
DataOpsError: API 请求失败时
|
|
40
|
+
AuthenticationError: 未认证或认证失败时
|
|
41
|
+
"""
|
|
42
|
+
nodes = release_list_list(MetaType.SKILL)
|
|
43
|
+
if query:
|
|
44
|
+
nodes = filter_nodes_by_pattern(nodes, query, include_digest=True)
|
|
45
|
+
return nodes
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def service_get_skill_by_code(code: str) -> MetaSkill:
|
|
49
|
+
"""按编码获取技能详情.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
code: 技能编码.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
MetaSkill 实例(含 data 字段,已自动从 rawData 反序列化)
|
|
56
|
+
|
|
57
|
+
Raises:
|
|
58
|
+
NotFoundError: 找不到对应技能时
|
|
59
|
+
DataOpsError: API 返回错误时
|
|
60
|
+
AuthenticationError: 未认证或认证失败时
|
|
61
|
+
"""
|
|
62
|
+
return release_list_get_by_code(MetaType.SKILL, code, _class=DataSkill)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def service_get_skill_by_name(name: str) -> MetaSkill:
|
|
66
|
+
"""按业务名获取技能详情.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
name: 技能业务名.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
MetaSkill 实例
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
NotFoundError: 找不到对应技能时
|
|
76
|
+
DataOpsError: API 返回错误时
|
|
77
|
+
AuthenticationError: 未认证或认证失败时
|
|
78
|
+
"""
|
|
79
|
+
return release_list_get_by_name(MetaType.SKILL, name, _class=DataSkill)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def service_list_skill_files(skill_id: str) -> list[MetaRelease]:
|
|
83
|
+
"""列出指定技能下的所有子文件(type=skill_file, parent_id=skill_id).
|
|
84
|
+
|
|
85
|
+
Args:
|
|
86
|
+
skill_id: 技能的 meta ID(不是 code).
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
MetaRelease 对象列表(每个对应一个 skill_file 子文件)
|
|
90
|
+
|
|
91
|
+
Raises:
|
|
92
|
+
DataOpsError: API 请求失败时
|
|
93
|
+
AuthenticationError: 未认证或认证失败时
|
|
94
|
+
"""
|
|
95
|
+
return release_list_list(
|
|
96
|
+
MetaType.SKILL_FILE,
|
|
97
|
+
extra_conditions=[{"name": "parent_id", "value": skill_id, "alias": "mrl"}],
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _post_skill(url: str, body: dict | None = None, *, params: dict | None = None) -> dict:
|
|
102
|
+
"""通用 skill POST 请求,返回 ``resp_body['data']``.
|
|
103
|
+
|
|
104
|
+
Raises:
|
|
105
|
+
DataOpsError: API 返回错误时
|
|
106
|
+
AuthenticationError: 未认证或认证失败时
|
|
107
|
+
"""
|
|
108
|
+
try:
|
|
109
|
+
with get_session() as session:
|
|
110
|
+
kwargs: dict = {}
|
|
111
|
+
if body is not None:
|
|
112
|
+
kwargs["content"] = to_json(encrypt_body(body))
|
|
113
|
+
if params is not None:
|
|
114
|
+
kwargs["params"] = params
|
|
115
|
+
resp = session.post(url, **kwargs)
|
|
116
|
+
resp_body = resp.json()
|
|
117
|
+
if resp.status_code == 401:
|
|
118
|
+
raise AuthenticationError("认证失败,请先登录")
|
|
119
|
+
if resp.status_code != 200:
|
|
120
|
+
raise DataOpsError(
|
|
121
|
+
resp_body.get("msg", f"HTTP {resp.status_code}"), resp.status_code
|
|
122
|
+
)
|
|
123
|
+
return resp_body.get("data") or {}
|
|
124
|
+
except SystemExit as e:
|
|
125
|
+
raise AuthenticationError("认证失败,请先登录") from e
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def service_import_skill(
|
|
129
|
+
file_path: str | Path,
|
|
130
|
+
sub_type: str = "llm",
|
|
131
|
+
name: str | None = None,
|
|
132
|
+
title: str | None = None,
|
|
133
|
+
ref_node_id: str | None = None,
|
|
134
|
+
ref_type: str | None = None,
|
|
135
|
+
) -> dict:
|
|
136
|
+
"""从 ZIP 包导入技能(multipart 上传).
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
file_path: 本地 ZIP 或 .md 文件路径.
|
|
140
|
+
sub_type: 技能子类型,可选 ``llm`` / ``api`` / ``report``,默认 ``llm``.
|
|
141
|
+
name: 可选覆盖 SKILL.md frontmatter 中的 ``name`` 字段.
|
|
142
|
+
title: 可选覆盖 SKILL.md 中的 ``title`` 字段.
|
|
143
|
+
ref_node_id: 分组节点 ID;指定后导入的技能会挂到该分组下.
|
|
144
|
+
ref_type: 相对位置 ``before`` / ``after``,仅在 ``ref_node_id`` 非空时生效.
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
后端返回的 ``data`` 字典(通常为 ``GroupTree``).
|
|
148
|
+
|
|
149
|
+
Raises:
|
|
150
|
+
DataOpsError: API 返回错误时
|
|
151
|
+
AuthenticationError: 未认证或认证失败时
|
|
152
|
+
FileNotFoundError: 本地文件不存在时
|
|
153
|
+
"""
|
|
154
|
+
path = Path(file_path)
|
|
155
|
+
if not path.exists():
|
|
156
|
+
raise FileNotFoundError(f"文件不存在: {path}")
|
|
157
|
+
|
|
158
|
+
form_data: dict[str, str] = {"subType": sub_type}
|
|
159
|
+
if name:
|
|
160
|
+
form_data["name"] = name
|
|
161
|
+
if title:
|
|
162
|
+
form_data["title"] = title
|
|
163
|
+
if ref_node_id:
|
|
164
|
+
form_data["refNodeId"] = ref_node_id
|
|
165
|
+
if ref_type:
|
|
166
|
+
form_data["refType"] = ref_type
|
|
167
|
+
|
|
168
|
+
with (
|
|
169
|
+
open(path, "rb") as f,
|
|
170
|
+
post_multipart(
|
|
171
|
+
"/data/skill/import",
|
|
172
|
+
files={"file": (path.name, f)},
|
|
173
|
+
data=form_data,
|
|
174
|
+
) as resp,
|
|
175
|
+
):
|
|
176
|
+
body = resp.json()
|
|
177
|
+
return body.get("data") or {}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def service_delete_skill(name: str) -> dict:
|
|
181
|
+
"""按业务名删除技能(级联删除所有 skill_file).
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
name: 技能业务名(注意不是 code).
|
|
185
|
+
|
|
186
|
+
Returns:
|
|
187
|
+
后端返回的 ``data`` 字典(通常为 ``GroupTree``).
|
|
188
|
+
|
|
189
|
+
Raises:
|
|
190
|
+
DataOpsError: API 返回错误时
|
|
191
|
+
AuthenticationError: 未认证或认证失败时
|
|
192
|
+
"""
|
|
193
|
+
return _post_skill(f"/data/skill/delete/{name}")
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
def service_upload_sub_file(
|
|
197
|
+
skill_name: str,
|
|
198
|
+
file_path: str | Path,
|
|
199
|
+
path: str,
|
|
200
|
+
) -> None:
|
|
201
|
+
"""为已存在技能上传子文件(doc / code / image 三类之一).
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
skill_name: 目标技能的业务名.
|
|
205
|
+
file_path: 本地文件路径.
|
|
206
|
+
path: 在技能包内的相对路径,如 ``docs/readme.md`` / ``script/main.py``.
|
|
207
|
+
|
|
208
|
+
Raises:
|
|
209
|
+
DataOpsError: API 返回错误时
|
|
210
|
+
AuthenticationError: 未认证或认证失败时
|
|
211
|
+
FileNotFoundError: 本地文件不存在时
|
|
212
|
+
"""
|
|
213
|
+
fp = Path(file_path)
|
|
214
|
+
if not fp.exists():
|
|
215
|
+
raise FileNotFoundError(f"文件不存在: {fp}")
|
|
216
|
+
|
|
217
|
+
with (
|
|
218
|
+
open(fp, "rb") as f,
|
|
219
|
+
post_multipart(
|
|
220
|
+
f"/data/skill/{skill_name}/subfile",
|
|
221
|
+
files={"file": (fp.name, f)},
|
|
222
|
+
data={"path": path},
|
|
223
|
+
) as resp,
|
|
224
|
+
):
|
|
225
|
+
resp.json() # 仅校验响应
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def service_delete_sub_file(skill_name: str, path: str) -> None:
|
|
229
|
+
"""删除技能下的指定子文件.
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
skill_name: 目标技能的业务名.
|
|
233
|
+
path: 子文件相对路径(与上传时一致).
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
DataOpsError: API 返回错误时
|
|
237
|
+
AuthenticationError: 未认证或认证失败时
|
|
238
|
+
"""
|
|
239
|
+
_post_skill(f"/data/skill/{skill_name}/subfile/delete", params={"path": path})
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def service_call_skill_by_code(code: str, inputs: Mapping) -> SkillCallResult:
|
|
243
|
+
"""调用 skill 同步执行,返回 Python 容器运行结果.
|
|
244
|
+
|
|
245
|
+
对应后端 ``POST /data/skill/callByCode/{code}``,入参 ``{"inputs": {...}}``。
|
|
246
|
+
|
|
247
|
+
Args:
|
|
248
|
+
code: 技能编码.
|
|
249
|
+
inputs: 传给 Python 容器的入参 dict.
|
|
250
|
+
|
|
251
|
+
Returns:
|
|
252
|
+
SkillCallResult 实例,含 ``inst_id`` / ``status`` / ``output_data`` / ``error_message``.
|
|
253
|
+
|
|
254
|
+
Raises:
|
|
255
|
+
DataOpsError: API 返回错误或响应 data 非 dict 时
|
|
256
|
+
AuthenticationError: 未认证或认证失败时
|
|
257
|
+
"""
|
|
258
|
+
try:
|
|
259
|
+
with get_session() as session:
|
|
260
|
+
resp = session.post(
|
|
261
|
+
f"/data/skill/callByCode/{code}",
|
|
262
|
+
content=to_json(encrypt_body({"inputs": dict(inputs)})),
|
|
263
|
+
)
|
|
264
|
+
resp_body = resp.json()
|
|
265
|
+
if resp.status_code == 401:
|
|
266
|
+
raise AuthenticationError("认证失败,请先登录")
|
|
267
|
+
if resp.status_code != 200:
|
|
268
|
+
raise DataOpsError(
|
|
269
|
+
resp_body.get("msg", f"HTTP {resp.status_code}"), resp.status_code
|
|
270
|
+
)
|
|
271
|
+
data = resp_body.get("data")
|
|
272
|
+
if not isinstance(data, dict):
|
|
273
|
+
raise DataOpsError(f"返回 data 格式异常: {type(data).__name__}")
|
|
274
|
+
return SkillCallResult.model_validate(data)
|
|
275
|
+
except SystemExit as e:
|
|
276
|
+
raise AuthenticationError("认证失败,请先登录") from e
|
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from pydantic import ConfigDict, JsonValue, TypeAdapter
|
|
6
|
+
from pydantic_core import to_json
|
|
7
|
+
|
|
8
|
+
from dataops_sdk.exceptions import DataOpsError
|
|
9
|
+
from dataops_sdk.schemas import (
|
|
10
|
+
DataDataBase,
|
|
11
|
+
Meta,
|
|
12
|
+
MetaRelease,
|
|
13
|
+
MetaType,
|
|
14
|
+
PageParam,
|
|
15
|
+
SqlQueryParam,
|
|
16
|
+
)
|
|
17
|
+
from dataops_sdk.schemas.enums import DataSourceType
|
|
18
|
+
from dataops_sdk.services.datasource import service_get_datasource_by_name
|
|
19
|
+
from dataops_sdk.services.meta import (
|
|
20
|
+
release_list_get_by_code,
|
|
21
|
+
release_list_get_by_name,
|
|
22
|
+
release_list_list,
|
|
23
|
+
)
|
|
24
|
+
from dataops_sdk.session import get_session
|
|
25
|
+
from dataops_sdk.utils import encrypt_body
|
|
26
|
+
|
|
27
|
+
# 允许的读操作 SQL 关键字前缀
|
|
28
|
+
_READ_ONLY_PREFIXES = ("select", "show", "describe", "explain", "use")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def validate_read_only_sql(sql: str) -> None:
|
|
32
|
+
"""校验 SQL 是否为只读操作.
|
|
33
|
+
|
|
34
|
+
Raises:
|
|
35
|
+
ValueError: SQL 为修改操作时
|
|
36
|
+
"""
|
|
37
|
+
normalized = sql.lower().strip().strip("(")
|
|
38
|
+
global _READ_ONLY_PREFIXES
|
|
39
|
+
if not normalized.startswith(_READ_ONLY_PREFIXES):
|
|
40
|
+
raise ValueError(f"仅支持 {'/'.join(_READ_ONLY_PREFIXES)} 语句")
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# LIMIT/OFFSET 模式(不区分大小写)
|
|
44
|
+
# 匹配 LIMIT n [OFFSET n]、LIMIT n, n、LIMIT n OFFSET n 形式
|
|
45
|
+
_LIMIT_PATTERN = re.compile(
|
|
46
|
+
r"\s+LIMIT\s+(?:\d+|\?)\s*(?:,\s*(?:\d+|\?))?\s*(?:OFFSET\s+(?:\d+|\?))?",
|
|
47
|
+
re.IGNORECASE,
|
|
48
|
+
)
|
|
49
|
+
_OFFSET_PATTERN = re.compile(
|
|
50
|
+
r"\s+OFFSET\s+(?:\d+|\?)\s*(?:ROWS)?",
|
|
51
|
+
re.IGNORECASE,
|
|
52
|
+
)
|
|
53
|
+
# Oracle FETCH 模式
|
|
54
|
+
_ORACLE_FETCH_PATTERN = re.compile(
|
|
55
|
+
r"\s+OFFSET\s+(?:\d+|\?)\s+ROWS\s+FETCH\s+NEXT\s+(?:\d+|\?)\s+ROWS\s+ONLY",
|
|
56
|
+
re.IGNORECASE,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def remove_limit_offset(sql: str, dialect: DataSourceType) -> str:
|
|
61
|
+
"""移除 SQL 中的 LIMIT/OFFSET 子句.
|
|
62
|
+
|
|
63
|
+
Args:
|
|
64
|
+
sql: 原始 SQL
|
|
65
|
+
dialect: 数据库类型
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
移除 LIMIT/OFFSET 后的 SQL
|
|
69
|
+
"""
|
|
70
|
+
if dialect == DataSourceType.ORACLE:
|
|
71
|
+
# Oracle: 移除 OFFSET ... ROWS FETCH NEXT ... ROWS ONLY
|
|
72
|
+
sql = _ORACLE_FETCH_PATTERN.sub("", sql)
|
|
73
|
+
elif dialect in {
|
|
74
|
+
DataSourceType.MYSQL,
|
|
75
|
+
DataSourceType.POSTGRESQL,
|
|
76
|
+
DataSourceType.DM,
|
|
77
|
+
DataSourceType.DORIS,
|
|
78
|
+
}:
|
|
79
|
+
# MySQL/PostgreSQL/DM/DORIS: 分别移除 LIMIT 和 OFFSET
|
|
80
|
+
sql = _LIMIT_PATTERN.sub("", sql)
|
|
81
|
+
sql = _OFFSET_PATTERN.sub("", sql)
|
|
82
|
+
else:
|
|
83
|
+
raise NotImplementedError(f"不支持 {dialect} 数据库类型")
|
|
84
|
+
return sql.strip()
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def service_list_tables(datasource: str) -> list[MetaRelease]:
|
|
88
|
+
"""搜索数据源中的表.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
datasource: 数据源名称
|
|
92
|
+
|
|
93
|
+
Returns:
|
|
94
|
+
MetaRelease 对象列表
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
DataOpsError: API 请求失败时
|
|
98
|
+
AuthenticationError: 未认证时
|
|
99
|
+
"""
|
|
100
|
+
headers = {"x-sub-system": datasource}
|
|
101
|
+
filters = (SqlQueryParam().add_eq("sub_system", datasource, "mrl"),)
|
|
102
|
+
return release_list_list(MetaType.TABLE, filters=filters, headers=headers)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def service_count_table_by_code(
|
|
106
|
+
code: str,
|
|
107
|
+
query_param: SqlQueryParam | None = None,
|
|
108
|
+
datasource: str | None = None,
|
|
109
|
+
) -> int:
|
|
110
|
+
"""获取表数据数量.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
code: 表编码
|
|
114
|
+
query_param: 查询条件
|
|
115
|
+
|
|
116
|
+
Returns:
|
|
117
|
+
表数据数量
|
|
118
|
+
|
|
119
|
+
Raises:
|
|
120
|
+
NotFoundError: 表不存在时
|
|
121
|
+
DataOpsError: API 请求失败时
|
|
122
|
+
AuthenticationError: 未认证时
|
|
123
|
+
"""
|
|
124
|
+
url = "/data/table/countData"
|
|
125
|
+
if query_param is None:
|
|
126
|
+
query_param = SqlQueryParam()
|
|
127
|
+
|
|
128
|
+
if not datasource:
|
|
129
|
+
meta = release_list_get_by_code(MetaType.TABLE, code, _class=DataDataBase)
|
|
130
|
+
datasource = meta.subSystem
|
|
131
|
+
|
|
132
|
+
body = {
|
|
133
|
+
"subSystem": datasource,
|
|
134
|
+
"code": code,
|
|
135
|
+
"type": MetaType.TABLE,
|
|
136
|
+
"queryParam": query_param,
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
with get_session() as session:
|
|
140
|
+
resp = session.post(url, content=to_json(encrypt_body(body)))
|
|
141
|
+
resp_body = resp.json()
|
|
142
|
+
if resp.status_code != 200:
|
|
143
|
+
raise DataOpsError(f"查询失败,{resp_body.get('msg')}", resp.status_code)
|
|
144
|
+
if resp_body.get("data") is None:
|
|
145
|
+
return 0
|
|
146
|
+
return resp_body["data"]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def service_count_table_by_name(
|
|
150
|
+
datasource: str,
|
|
151
|
+
name: str,
|
|
152
|
+
query_param: SqlQueryParam | None = None,
|
|
153
|
+
) -> int:
|
|
154
|
+
"""通过数据源 + 表名获取表数据数量.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
datasource: 数据源名称
|
|
158
|
+
name: 表名
|
|
159
|
+
query_param: 查询条件
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
表数据数量
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
NotFoundError: 表不存在时
|
|
166
|
+
DataOpsError: API 请求失败时
|
|
167
|
+
AuthenticationError: 未认证时
|
|
168
|
+
"""
|
|
169
|
+
headers = {"x-sub-system": datasource}
|
|
170
|
+
meta = release_list_get_by_name(MetaType.TABLE, name, headers=headers)
|
|
171
|
+
return service_count_table_by_code(meta.code, query_param, datasource=datasource)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def service_get_table_data_by_code(
|
|
175
|
+
code: str,
|
|
176
|
+
page_param: PageParam | None = None,
|
|
177
|
+
query_param: SqlQueryParam | None = None,
|
|
178
|
+
datasource: str | None = None,
|
|
179
|
+
) -> list[Mapping[str, JsonValue]]:
|
|
180
|
+
"""查询表数据.
|
|
181
|
+
|
|
182
|
+
Args:
|
|
183
|
+
code: 表编码
|
|
184
|
+
page_param: 分页参数
|
|
185
|
+
query_param: 查询条件
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
表数据行列表(每行为字典)
|
|
189
|
+
|
|
190
|
+
Raises:
|
|
191
|
+
NotFoundError: 表不存在时
|
|
192
|
+
DataOpsError: API 请求失败时
|
|
193
|
+
AuthenticationError: 未认证时
|
|
194
|
+
"""
|
|
195
|
+
url = "/data/table/findData"
|
|
196
|
+
if page_param is None:
|
|
197
|
+
page_param = PageParam()
|
|
198
|
+
if query_param is None:
|
|
199
|
+
query_param = SqlQueryParam()
|
|
200
|
+
|
|
201
|
+
if not datasource:
|
|
202
|
+
meta = release_list_get_by_code(MetaType.TABLE, code, _class=DataDataBase)
|
|
203
|
+
datasource = meta.subSystem
|
|
204
|
+
|
|
205
|
+
body = {
|
|
206
|
+
"subSystem": datasource,
|
|
207
|
+
"code": code,
|
|
208
|
+
"pageParam": page_param,
|
|
209
|
+
"queryParam": query_param,
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
with get_session(timeout=15) as session:
|
|
213
|
+
resp = session.post(url, content=to_json(encrypt_body(body)))
|
|
214
|
+
resp_body = resp.json()
|
|
215
|
+
if resp.status_code != 200:
|
|
216
|
+
raise DataOpsError(f"查询失败,{resp_body.get('msg')}", resp.status_code)
|
|
217
|
+
adapter = TypeAdapter(list[Mapping[str, JsonValue]], config=ConfigDict(str_to_lower=True))
|
|
218
|
+
|
|
219
|
+
return adapter.validate_python(resp_body.get("data", []))
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def service_get_table_data_by_name(
|
|
223
|
+
datasource: str,
|
|
224
|
+
name: str,
|
|
225
|
+
page_param: PageParam | None = None,
|
|
226
|
+
query_param: SqlQueryParam | None = None,
|
|
227
|
+
) -> list[dict[str, Any]]:
|
|
228
|
+
headers = {"x-sub-system": datasource}
|
|
229
|
+
meta = release_list_get_by_name(MetaType.TABLE, name, headers=headers, _class=DataDataBase)
|
|
230
|
+
return service_get_table_data_by_code(meta.code, page_param, query_param, datasource)
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def service_get_table_detail_by_name(
|
|
234
|
+
datasource: str,
|
|
235
|
+
name: str,
|
|
236
|
+
) -> Meta[DataDataBase]:
|
|
237
|
+
"""指定数据源表详情.
|
|
238
|
+
|
|
239
|
+
Args:
|
|
240
|
+
datasource: 数据源名称
|
|
241
|
+
name: 表名
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
数据源 DDL 模型
|
|
245
|
+
|
|
246
|
+
Raises:
|
|
247
|
+
ValueError: 无效类型
|
|
248
|
+
NotFoundError: 数据源不存在
|
|
249
|
+
DataOpsError: API 请求失败
|
|
250
|
+
AuthenticationError: 未认证
|
|
251
|
+
"""
|
|
252
|
+
headers = {"x-sub-system": datasource}
|
|
253
|
+
meta = release_list_get_by_name(MetaType.TABLE, name, headers=headers, _class=DataDataBase)
|
|
254
|
+
return meta
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def service_get_table_detail_by_code(code: str) -> Meta[DataDataBase]:
|
|
258
|
+
"""指定数据源表详情.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
code: 表编码
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
数据源 DDL 模型
|
|
265
|
+
|
|
266
|
+
Raises:
|
|
267
|
+
ValueError: 无效类型
|
|
268
|
+
NotFoundError: 数据源不存在
|
|
269
|
+
DataOpsError: API 请求失败
|
|
270
|
+
AuthenticationError: 未认证
|
|
271
|
+
"""
|
|
272
|
+
meta = release_list_get_by_code(MetaType.TABLE, code, _class=DataDataBase)
|
|
273
|
+
return meta
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def service_exec_sql_by_datasource(
|
|
277
|
+
datasource: str,
|
|
278
|
+
sql: str,
|
|
279
|
+
page_num: int = 0,
|
|
280
|
+
page_size: int = 20,
|
|
281
|
+
) -> list[Mapping[str, JsonValue]]:
|
|
282
|
+
"""执行任意 SQL 查询.
|
|
283
|
+
|
|
284
|
+
Args:
|
|
285
|
+
datasource: 数据源名称
|
|
286
|
+
sql: SQL 查询语句
|
|
287
|
+
page_num: 页码 [default: 0]
|
|
288
|
+
page_size: 每页大小 [default: 20]
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
查询结果行列表(每行为字典)
|
|
292
|
+
|
|
293
|
+
Raises:
|
|
294
|
+
NotFoundError: 表不存在时
|
|
295
|
+
DataOpsError: API 请求失败时
|
|
296
|
+
AuthenticationError: 未认证时
|
|
297
|
+
ValueError: SQL 为修改操作时
|
|
298
|
+
"""
|
|
299
|
+
if page_num < 0:
|
|
300
|
+
raise ValueError("page_num must >= 0")
|
|
301
|
+
if page_size <= 0:
|
|
302
|
+
raise ValueError("page_size must > 0")
|
|
303
|
+
validate_read_only_sql(sql)
|
|
304
|
+
meta = service_get_datasource_by_name(datasource)
|
|
305
|
+
sql = paginate_query(meta.data.type, sql, page_size, page_num * page_size)
|
|
306
|
+
# Lazy import:依赖 dataops_sdk[runner] extra,仅在调用本函数时才需要安装
|
|
307
|
+
from dataops_sdk.runner import DatabaseConn
|
|
308
|
+
|
|
309
|
+
with DatabaseConn(datasource, log_sql=False) as conn:
|
|
310
|
+
results = conn.select(sql, size=page_size)
|
|
311
|
+
return results
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def paginate_query(dialect: DataSourceType, base_sql: str, limit: int, offset: int):
|
|
315
|
+
"""通用的分页函数"""
|
|
316
|
+
# 不同方言的分页包装
|
|
317
|
+
if dialect in {
|
|
318
|
+
DataSourceType.MYSQL,
|
|
319
|
+
DataSourceType.POSTGRESQL,
|
|
320
|
+
DataSourceType.DM,
|
|
321
|
+
DataSourceType.DORIS,
|
|
322
|
+
}:
|
|
323
|
+
# 检测 + 移除 原来有的 LIMIT/OFFSET
|
|
324
|
+
base_sql = remove_limit_offset(base_sql, dialect)
|
|
325
|
+
|
|
326
|
+
# 修改 SQL
|
|
327
|
+
sql = f"{base_sql} LIMIT {limit} OFFSET {offset}"
|
|
328
|
+
elif dialect == DataSourceType.ORACLE:
|
|
329
|
+
# 检测 + 移除 原来有的 LIMIT/OFFSET
|
|
330
|
+
base_sql = remove_limit_offset(base_sql, dialect)
|
|
331
|
+
|
|
332
|
+
# 修改 SQL(内层子查询加别名避免 ORA-00918)
|
|
333
|
+
sql = f"""
|
|
334
|
+
SELECT * FROM (
|
|
335
|
+
{base_sql}
|
|
336
|
+
) _t OFFSET {offset} ROWS FETCH NEXT {limit} ROWS ONLY
|
|
337
|
+
"""
|
|
338
|
+
else:
|
|
339
|
+
raise ValueError(f"Unsupported database: {dialect}")
|
|
340
|
+
return sql
|