data-ops-cli 0.4.1__tar.gz → 0.4.3__tar.gz
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.
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/PKG-INFO +1 -1
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/data_ops_cli.egg-info/PKG-INFO +1 -1
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/data_ops_cli.egg-info/SOURCES.txt +2 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/__init__.py +1 -1
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/auth.py +7 -4
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/dataset.py +95 -3
- data_ops_cli-0.4.3/src/dataops_cli/cli/dict_.py +219 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/factory.py +4 -0
- data_ops_cli-0.4.3/src/dataops_cli/cli/kb.py +234 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/ref.py +5 -20
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/skill.py +38 -18
- data_ops_cli-0.4.3/src/dataops_cli/cli/tag.py +297 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/workflow.py +10 -6
- data_ops_cli-0.4.1/src/dataops_cli/cli/dict_.py +0 -101
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/README.md +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/pyproject.toml +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/setup.cfg +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/data_ops_cli.egg-info/dependency_links.txt +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/data_ops_cli.egg-info/entry_points.txt +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/data_ops_cli.egg-info/requires.txt +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/data_ops_cli.egg-info/top_level.txt +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/_compat.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/__init__.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/alarm.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/api.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/data_table.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/datasource.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/hazard.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/metrics.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/report.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/table.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/utils.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/cli/widget.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/exceptions.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/runner.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/schems.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/sdk.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/session.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/src/dataops_cli/utils.py +0 -0
- {data_ops_cli-0.4.1 → data_ops_cli-0.4.3}/tests/test_table_relation.py +0 -0
|
@@ -24,11 +24,13 @@ src/dataops_cli/cli/datasource.py
|
|
|
24
24
|
src/dataops_cli/cli/dict_.py
|
|
25
25
|
src/dataops_cli/cli/factory.py
|
|
26
26
|
src/dataops_cli/cli/hazard.py
|
|
27
|
+
src/dataops_cli/cli/kb.py
|
|
27
28
|
src/dataops_cli/cli/metrics.py
|
|
28
29
|
src/dataops_cli/cli/ref.py
|
|
29
30
|
src/dataops_cli/cli/report.py
|
|
30
31
|
src/dataops_cli/cli/skill.py
|
|
31
32
|
src/dataops_cli/cli/table.py
|
|
33
|
+
src/dataops_cli/cli/tag.py
|
|
32
34
|
src/dataops_cli/cli/utils.py
|
|
33
35
|
src/dataops_cli/cli/widget.py
|
|
34
36
|
src/dataops_cli/cli/workflow.py
|
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
from typing import Annotated
|
|
10
10
|
|
|
11
11
|
import typer
|
|
12
|
+
from dataops_cli.cli.utils import print_list
|
|
12
13
|
from dataops_sdk.constants import APPLICATION_FILE, USER_INFO_FILE
|
|
13
14
|
from dataops_sdk.exceptions import AuthenticationError
|
|
14
15
|
from dataops_sdk.schemas import UserInfo
|
|
@@ -25,8 +26,6 @@ from dataops_sdk.utils import get_work_dir
|
|
|
25
26
|
from rich.console import Console
|
|
26
27
|
from rich.table import Table
|
|
27
28
|
|
|
28
|
-
from dataops_cli.cli.utils import print_list
|
|
29
|
-
|
|
30
29
|
auth_typer = typer.Typer(name="auth", help="认证相关命令", no_args_is_help=True)
|
|
31
30
|
application_typer = typer.Typer(name="application", help="应用相关命令", no_args_is_help=True)
|
|
32
31
|
auth_typer.add_typer(application_typer)
|
|
@@ -60,9 +59,13 @@ def auth_status() -> None:
|
|
|
60
59
|
print(f"{user_info.true_name}({user_info.user_name})登录过期,请重新登录")
|
|
61
60
|
else:
|
|
62
61
|
if application is None:
|
|
63
|
-
|
|
62
|
+
typer.echo(
|
|
63
|
+
f"{user_info.true_name}({user_info.user_name})已登录, 应用未设置\n请执行 data-ops-cli auth application set <应用code> 设置应用",
|
|
64
|
+
err=True,
|
|
65
|
+
)
|
|
66
|
+
raise typer.Exit(code=1)
|
|
64
67
|
else:
|
|
65
|
-
|
|
68
|
+
typer.echo(
|
|
66
69
|
f"{user_info.true_name}({user_info.user_name})已登录, 应用设置为{application.LABEL_CN}({application.CODE})"
|
|
67
70
|
)
|
|
68
71
|
|
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
"""数据集管理命令组.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
提供数据集的列表查询、详情、数据量统计与实际数据获取。
|
|
4
4
|
"""
|
|
5
5
|
|
|
6
|
+
from pathlib import Path
|
|
6
7
|
from typing import Annotated
|
|
7
8
|
|
|
8
9
|
import typer
|
|
10
|
+
from pydantic_core import to_json
|
|
11
|
+
|
|
9
12
|
from dataops_sdk.exceptions import DataOpsError
|
|
10
|
-
from dataops_sdk.schemas import PageParam, SqlFilter, SqlQueryParam
|
|
13
|
+
from dataops_sdk.schemas import MetaType, PageParam, SqlFilter, SqlQueryParam
|
|
11
14
|
from dataops_sdk.services import (
|
|
15
|
+
release_list_get_by_code,
|
|
12
16
|
release_list_update,
|
|
13
17
|
service_count_dataset_by_code,
|
|
14
18
|
service_get_dataset_data_by_code,
|
|
15
19
|
service_get_dataset_detail_by_code,
|
|
16
20
|
service_list_datasets,
|
|
17
21
|
)
|
|
18
|
-
from dataops_sdk.
|
|
22
|
+
from dataops_sdk.session import get_session
|
|
23
|
+
from dataops_sdk.utils import encrypt_body, filter_nodes_by_pattern
|
|
19
24
|
|
|
20
25
|
from dataops_cli.cli.utils import print_list, print_table_column_detail
|
|
21
26
|
|
|
27
|
+
# 数据集 rawData 中承载 SQL 定义的字段名(subType=sql 的数据集均有此字段)。
|
|
28
|
+
SQL_FIELD = "text"
|
|
29
|
+
|
|
30
|
+
|
|
22
31
|
dataset_typer = typer.Typer(name="dataset", help="Manage dataset.", no_args_is_help=True)
|
|
23
32
|
update_typer = typer.Typer(name="update", help="变更", no_args_is_help=True)
|
|
24
33
|
dataset_typer.add_typer(update_typer)
|
|
25
34
|
column_typer = typer.Typer(name="column", help="列变更", no_args_is_help=True)
|
|
26
35
|
update_typer.add_typer(column_typer)
|
|
36
|
+
sql_typer = typer.Typer(name="sql", help="SQL 编辑", no_args_is_help=True)
|
|
37
|
+
update_typer.add_typer(sql_typer)
|
|
27
38
|
|
|
28
39
|
|
|
29
40
|
@dataset_typer.command("list")
|
|
@@ -241,3 +252,84 @@ def update_dataset_column(
|
|
|
241
252
|
typer.echo(f"错误: {e.message}")
|
|
242
253
|
raise typer.Exit(code=1) from None
|
|
243
254
|
typer.echo(f"修改数据集 {code} 字段 {column_item.name} 完成")
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def _load_sql(code: str):
|
|
258
|
+
"""校验数据集存在 SQL 字段并返回 Meta;失败统一报错退出."""
|
|
259
|
+
try:
|
|
260
|
+
meta = release_list_get_by_code(MetaType.DATASET, code)
|
|
261
|
+
except DataOpsError as e:
|
|
262
|
+
typer.echo(f"错误: {e.message}", err=True)
|
|
263
|
+
raise typer.Exit(code=1) from None
|
|
264
|
+
data = meta.data
|
|
265
|
+
if not isinstance(data, dict) or not isinstance(data.get(SQL_FIELD), str):
|
|
266
|
+
typer.echo(f"数据集 {code} 未找到 SQL 字段({SQL_FIELD})", err=True)
|
|
267
|
+
raise typer.Exit(code=1)
|
|
268
|
+
return meta
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _save_sql(meta, sql: str) -> None:
|
|
272
|
+
"""整段替换 SQL 并保存:先解析字段回填 columns(失败仅警告),保存失败统一报错."""
|
|
273
|
+
meta.data[SQL_FIELD] = sql
|
|
274
|
+
try:
|
|
275
|
+
meta_payload = meta.model_dump(mode="json", exclude={"data"})
|
|
276
|
+
meta_payload["rawData"] = to_json(meta.data).decode()
|
|
277
|
+
with get_session() as session:
|
|
278
|
+
resp = session.post(
|
|
279
|
+
"/data/dataset/analysis",
|
|
280
|
+
content=to_json(encrypt_body({"meta": meta_payload, "params": []})),
|
|
281
|
+
)
|
|
282
|
+
resp.raise_for_status()
|
|
283
|
+
meta.data["columns"] = resp.json()["data"]["columns"]
|
|
284
|
+
typer.echo(f"字段解析完成({len(meta.data['columns'])} 列)")
|
|
285
|
+
except Exception as e:
|
|
286
|
+
typer.echo(f"⚠️ 字段解析失败,仅保存 SQL: {e}", err=True)
|
|
287
|
+
try:
|
|
288
|
+
release_list_update(meta)
|
|
289
|
+
except DataOpsError as e:
|
|
290
|
+
typer.echo(f"错误: {e.message}", err=True)
|
|
291
|
+
raise typer.Exit(code=1) from None
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
@sql_typer.command("edit", no_args_is_help=True)
|
|
295
|
+
def edit_dataset_sql(
|
|
296
|
+
code: Annotated[str, typer.Argument(help="数据集编码")],
|
|
297
|
+
old_texts: Annotated[
|
|
298
|
+
list[str],
|
|
299
|
+
typer.Option("--old-text", help="待替换文本(可多次传入,与 --new-text 一一对应)"),
|
|
300
|
+
],
|
|
301
|
+
new_texts: Annotated[
|
|
302
|
+
list[str],
|
|
303
|
+
typer.Option("--new-text", help="替换后的文本(与 --old-text 一一对应)"),
|
|
304
|
+
],
|
|
305
|
+
) -> None:
|
|
306
|
+
"""按 --old-text/--new-text 成对精准替换 SQL 文本."""
|
|
307
|
+
if len(old_texts) != len(new_texts):
|
|
308
|
+
typer.echo("参数 --old-text 与 --new-text 数量不一致", err=True)
|
|
309
|
+
raise typer.Exit(code=1)
|
|
310
|
+
meta = _load_sql(code)
|
|
311
|
+
content = meta.data[SQL_FIELD]
|
|
312
|
+
for idx, (old, new) in enumerate(zip(old_texts, new_texts), start=1):
|
|
313
|
+
if old not in content:
|
|
314
|
+
typer.echo(f"[{idx}] 未找到匹配文本 {old}", err=True)
|
|
315
|
+
raise typer.Exit(code=1)
|
|
316
|
+
content = content.replace(old, new, count=1)
|
|
317
|
+
_save_sql(meta, content)
|
|
318
|
+
typer.echo(f"数据集 {code} SQL 更新成功")
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
@sql_typer.command("write", no_args_is_help=True)
|
|
322
|
+
def write_dataset_sql(
|
|
323
|
+
code: Annotated[str, typer.Argument(help="数据集编码")],
|
|
324
|
+
file: Annotated[Path | None, typer.Argument(help="输入文件路径")] = None,
|
|
325
|
+
content: Annotated[str | None, typer.Option("--content", help="SQL 内容")] = None,
|
|
326
|
+
) -> None:
|
|
327
|
+
"""整段写入 SQL,file 与 --content 至少指定一个."""
|
|
328
|
+
if file:
|
|
329
|
+
content = file.read_text(encoding="utf-8")
|
|
330
|
+
elif not content:
|
|
331
|
+
typer.echo("参数 file 或 --content 必须指定一个", err=True)
|
|
332
|
+
raise typer.Exit(code=1)
|
|
333
|
+
meta = _load_sql(code)
|
|
334
|
+
_save_sql(meta, content)
|
|
335
|
+
typer.echo(f"数据集 {code} SQL 写入成功")
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""字典(dict)管理命令组.
|
|
2
|
+
|
|
3
|
+
提供数据源字典的列表查询、详情查看、字典树以及字段增删改能力。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
from typing import Annotated
|
|
9
|
+
|
|
10
|
+
import typer
|
|
11
|
+
from dataops_sdk.exceptions import DataOpsError
|
|
12
|
+
from dataops_sdk.schemas import MetaType
|
|
13
|
+
from dataops_sdk.services.dict_ import (
|
|
14
|
+
service_get_dict_detail_by_code,
|
|
15
|
+
service_get_dict_detail_by_name,
|
|
16
|
+
service_list_dicts,
|
|
17
|
+
servive_tree_dict,
|
|
18
|
+
)
|
|
19
|
+
from dataops_sdk.services.meta import release_list_get_by_code, release_list_update
|
|
20
|
+
from dataops_sdk.utils import filter_nodes_by_pattern
|
|
21
|
+
|
|
22
|
+
from dataops_cli.cli.utils import print_group_tree, print_list
|
|
23
|
+
|
|
24
|
+
dict_typer = typer.Typer(name="dict", help="Manage dict.", no_args_is_help=True)
|
|
25
|
+
field_typer = typer.Typer(name="field", help="字典字段变更", no_args_is_help=True)
|
|
26
|
+
dict_typer.add_typer(field_typer)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _find_option_index(options: list, value: str) -> int | None:
|
|
30
|
+
"""在 options 列表中按 value 查找下标,未找到返回 None."""
|
|
31
|
+
return next((i for i, o in enumerate(options) if str(o.get("value")) == value), None)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dict_typer.command("list")
|
|
35
|
+
def list_dict(
|
|
36
|
+
datasource: Annotated[str, typer.Argument(help="数据源")],
|
|
37
|
+
query: Annotated[str | None, typer.Option("-q", "--query", help="字典名称关键字")] = None,
|
|
38
|
+
) -> None:
|
|
39
|
+
"""列出数据源中的字典列表。
|
|
40
|
+
|
|
41
|
+
支持通过关键字过滤字典名称,并支持正则表达式匹配。
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
datasource: 数据源名称。
|
|
45
|
+
query: 可选字典名称关键字(支持正则)。
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
# 列出指定数据源的所有字典
|
|
49
|
+
>>> data-ops-cli dict list <datasource>
|
|
50
|
+
|
|
51
|
+
# 列出数据源中名称包含 "user" 的字典
|
|
52
|
+
>>> data-ops-cli dict list <datasource> -q user
|
|
53
|
+
|
|
54
|
+
# 使用正则过滤字典名
|
|
55
|
+
>>> data-ops-cli dict list <datasource> -q "^user_.*"
|
|
56
|
+
"""
|
|
57
|
+
try:
|
|
58
|
+
group_nodes = service_list_dicts(datasource)
|
|
59
|
+
except DataOpsError as e:
|
|
60
|
+
typer.echo(f"错误: {e.message}")
|
|
61
|
+
raise typer.Exit(code=1) from None
|
|
62
|
+
|
|
63
|
+
if not group_nodes:
|
|
64
|
+
print("未查询到匹配字典")
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
if query:
|
|
68
|
+
group_nodes = filter_nodes_by_pattern(group_nodes, query)
|
|
69
|
+
|
|
70
|
+
print_list(
|
|
71
|
+
["code", "name", "alias"],
|
|
72
|
+
[[column.code, column.name, column.title] for column in group_nodes],
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
@dict_typer.command("detail", no_args_is_help=True)
|
|
77
|
+
def get_dict_detail(
|
|
78
|
+
arg1: Annotated[str, typer.Argument(help="编码或数据源")],
|
|
79
|
+
name: Annotated[str | None, typer.Argument(help="字典名称")] = None,
|
|
80
|
+
) -> None:
|
|
81
|
+
"""获取字典详情信息。
|
|
82
|
+
|
|
83
|
+
支持两种查询方式:
|
|
84
|
+
|
|
85
|
+
1. 通过字典编码(CODE)直接查询
|
|
86
|
+
2. 通过数据源名称和字典名称组合查询
|
|
87
|
+
|
|
88
|
+
默认只显示字典选项的基本信息(label/value)
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
arg1: 字典编码;当同时提供 ``name`` 时为数据源名称。
|
|
92
|
+
name: 可选字典名称。
|
|
93
|
+
|
|
94
|
+
Examples:
|
|
95
|
+
# 通过字典编码查询字典结构
|
|
96
|
+
>>> data-ops-cli dict detail dict_code_123
|
|
97
|
+
|
|
98
|
+
# 通过数据源和字典名查询
|
|
99
|
+
>>> data-ops-cli dict detail <datasource> my_dict
|
|
100
|
+
"""
|
|
101
|
+
try:
|
|
102
|
+
meta = (
|
|
103
|
+
service_get_dict_detail_by_name(arg1, name)
|
|
104
|
+
if name
|
|
105
|
+
else service_get_dict_detail_by_code(arg1)
|
|
106
|
+
)
|
|
107
|
+
except DataOpsError as e:
|
|
108
|
+
typer.echo(f"错误: {e.message}")
|
|
109
|
+
raise typer.Exit(code=1) from None
|
|
110
|
+
print(f"{meta.name}({meta.title})")
|
|
111
|
+
print_list(["value", "label"], [[option.value, option.label] for option in meta.data.options])
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dict_typer.command("tree")
|
|
115
|
+
def tree_dict(
|
|
116
|
+
query: Annotated[str | None, typer.Option("-q", "--query", help="字典名称关键字")] = None,
|
|
117
|
+
) -> None:
|
|
118
|
+
"""展示字典树结构。
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
query: 字典名称关键字(支持正则)。
|
|
122
|
+
"""
|
|
123
|
+
try:
|
|
124
|
+
tree = servive_tree_dict()
|
|
125
|
+
except DataOpsError as e:
|
|
126
|
+
typer.echo(f"错误: {e.message}")
|
|
127
|
+
raise typer.Exit(code=1) from None
|
|
128
|
+
print_group_tree(tree, query)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@field_typer.command("add", no_args_is_help=True)
|
|
132
|
+
def add_field(
|
|
133
|
+
code: Annotated[str, typer.Argument(help="字典编码")],
|
|
134
|
+
value: Annotated[str, typer.Argument(help="选项值")],
|
|
135
|
+
label: Annotated[str, typer.Argument(help="选项标签")],
|
|
136
|
+
) -> None:
|
|
137
|
+
"""新增字典字段。
|
|
138
|
+
|
|
139
|
+
Args:
|
|
140
|
+
code: 字典编码。
|
|
141
|
+
value: 选项值。
|
|
142
|
+
label: 选项标签。
|
|
143
|
+
|
|
144
|
+
Examples:
|
|
145
|
+
# 单条新增
|
|
146
|
+
>>> data-ops-cli dict field add dict_code_001 3 第三选项
|
|
147
|
+
"""
|
|
148
|
+
try:
|
|
149
|
+
meta = release_list_get_by_code(MetaType.DICT, code)
|
|
150
|
+
options = meta.data.setdefault("options", []) if meta.data else []
|
|
151
|
+
if any(str(o.get("value")) == value for o in options):
|
|
152
|
+
typer.echo(f"字典 {code} 中已存在 value={value}", err=True)
|
|
153
|
+
raise typer.Exit(code=1)
|
|
154
|
+
options.append({"value": value, "label": label, "id": ""})
|
|
155
|
+
release_list_update(meta, datasource=meta.subSystem)
|
|
156
|
+
except DataOpsError as e:
|
|
157
|
+
typer.echo(f"错误: {e.message}")
|
|
158
|
+
raise typer.Exit(code=1) from None
|
|
159
|
+
print(f"新增 {code} {value} 完成")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@field_typer.command("edit", no_args_is_help=True)
|
|
163
|
+
def edit_field(
|
|
164
|
+
code: Annotated[str, typer.Argument(help="字典编码")],
|
|
165
|
+
value: Annotated[str, typer.Argument(help="选项值")],
|
|
166
|
+
label: Annotated[str, typer.Argument(help="新标签")],
|
|
167
|
+
) -> None:
|
|
168
|
+
"""编辑字典字段标签。
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
code: 字典编码。
|
|
172
|
+
value: 要修改的选项值。
|
|
173
|
+
label: 新的标签名。
|
|
174
|
+
|
|
175
|
+
Examples:
|
|
176
|
+
>>> data-ops-cli dict field edit dict_code_001 0 禁用
|
|
177
|
+
"""
|
|
178
|
+
try:
|
|
179
|
+
meta = release_list_get_by_code(MetaType.DICT, code)
|
|
180
|
+
options = meta.data.get("options", []) if meta.data else []
|
|
181
|
+
idx = _find_option_index(options, value)
|
|
182
|
+
if idx is None:
|
|
183
|
+
typer.echo(f"字典中不存在选项: {value}", err=True)
|
|
184
|
+
raise typer.Exit(code=1)
|
|
185
|
+
options[idx]["label"] = label
|
|
186
|
+
release_list_update(meta, datasource=meta.subSystem)
|
|
187
|
+
except DataOpsError as e:
|
|
188
|
+
typer.echo(f"错误: {e.message}")
|
|
189
|
+
raise typer.Exit(code=1) from None
|
|
190
|
+
print(f"修改 {code} {value} 完成")
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
@field_typer.command("delete", no_args_is_help=True)
|
|
194
|
+
def delete_field(
|
|
195
|
+
code: Annotated[str, typer.Argument(help="字典编码")],
|
|
196
|
+
value: Annotated[str, typer.Argument(help="选项值")],
|
|
197
|
+
) -> None:
|
|
198
|
+
"""删除字典字段。
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
code: 字典编码。
|
|
202
|
+
value: 要删除的选项值。
|
|
203
|
+
|
|
204
|
+
Examples:
|
|
205
|
+
>>> data-ops-cli dict field delete dict_code_001 0
|
|
206
|
+
"""
|
|
207
|
+
try:
|
|
208
|
+
meta = release_list_get_by_code(MetaType.DICT, code)
|
|
209
|
+
options = meta.data.get("options", []) if meta.data else []
|
|
210
|
+
idx = _find_option_index(options, value)
|
|
211
|
+
if idx is None:
|
|
212
|
+
typer.echo(f"字典中不存在选项: {value}", err=True)
|
|
213
|
+
raise typer.Exit(code=1)
|
|
214
|
+
options.pop(idx)
|
|
215
|
+
release_list_update(meta, datasource=meta.subSystem)
|
|
216
|
+
except DataOpsError as e:
|
|
217
|
+
typer.echo(f"错误: {e.message}")
|
|
218
|
+
raise typer.Exit(code=1) from None
|
|
219
|
+
print(f"删除 {code} {value} 完成")
|
|
@@ -53,11 +53,13 @@ def create_app():
|
|
|
53
53
|
from dataops_cli.cli.datasource import datasource_typer
|
|
54
54
|
from dataops_cli.cli.dict_ import dict_typer
|
|
55
55
|
from dataops_cli.cli.hazard import hazard_typer
|
|
56
|
+
from dataops_cli.cli.kb import kb_typer
|
|
56
57
|
from dataops_cli.cli.metrics import metrics_typer
|
|
57
58
|
from dataops_cli.cli.ref import ref_typer
|
|
58
59
|
from dataops_cli.cli.report import report_typer
|
|
59
60
|
from dataops_cli.cli.skill import skill_typer
|
|
60
61
|
from dataops_cli.cli.table import table_typer
|
|
62
|
+
from dataops_cli.cli.tag import tag_typer
|
|
61
63
|
from dataops_cli.cli.widget import widget_typer
|
|
62
64
|
from dataops_cli.cli.workflow import workflow_typer
|
|
63
65
|
|
|
@@ -81,6 +83,8 @@ def create_app():
|
|
|
81
83
|
app.add_typer(widget_typer)
|
|
82
84
|
app.add_typer(dict_typer)
|
|
83
85
|
app.add_typer(hazard_typer)
|
|
86
|
+
app.add_typer(kb_typer)
|
|
87
|
+
app.add_typer(tag_typer)
|
|
84
88
|
|
|
85
89
|
app.registered_callback = TyperInfo(callback=main)
|
|
86
90
|
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
"""Knowledge Base(知识库)管理命令组.
|
|
2
|
+
|
|
3
|
+
- 服务层函数 (service_*):纯 Python 接口,返回原始数据
|
|
4
|
+
- CLI 命令:基于服务层的薄包装,负责格式化输出
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Annotated
|
|
11
|
+
|
|
12
|
+
import typer
|
|
13
|
+
from dataops_cli.cli.utils import print_list
|
|
14
|
+
from dataops_sdk.exceptions import DataOpsError
|
|
15
|
+
from dataops_sdk.schemas.wiki import KbType
|
|
16
|
+
from dataops_sdk.services.wiki import (
|
|
17
|
+
service_get_knowledge_base,
|
|
18
|
+
service_list_knowledge_bases,
|
|
19
|
+
service_list_knowledge_documents,
|
|
20
|
+
service_search_knowledge,
|
|
21
|
+
service_search_wiki_pages,
|
|
22
|
+
service_upload_knowledge_file,
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
kb_typer = typer.Typer(
|
|
26
|
+
name="kb",
|
|
27
|
+
help="知识库管理",
|
|
28
|
+
no_args_is_help=True,
|
|
29
|
+
)
|
|
30
|
+
doc_typer = typer.Typer(name="doc", help="知识库文档", no_args_is_help=True)
|
|
31
|
+
page_typer = typer.Typer(name="page", help="Wiki 页面", no_args_is_help=True)
|
|
32
|
+
kb_typer.add_typer(doc_typer)
|
|
33
|
+
kb_typer.add_typer(page_typer)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@kb_typer.command("list")
|
|
37
|
+
def kb_list_cmd(
|
|
38
|
+
kb_type: Annotated[
|
|
39
|
+
KbType | None,
|
|
40
|
+
typer.Option("--type", "-t", help="知识库类型:document / faq / wiki"),
|
|
41
|
+
] = None,
|
|
42
|
+
page: Annotated[int, typer.Option("--page", help="页码", min=1)] = 1,
|
|
43
|
+
page_size: Annotated[int, typer.Option("--page-size", help="每页条数", min=1)] = 20,
|
|
44
|
+
) -> None:
|
|
45
|
+
"""列出知识库。
|
|
46
|
+
|
|
47
|
+
Examples:
|
|
48
|
+
>>> data-ops-cli kb list
|
|
49
|
+
>>> data-ops-cli kb list --type wiki
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
result = service_list_knowledge_bases(page=page, page_size=page_size, kb_type=kb_type)
|
|
53
|
+
except DataOpsError as e:
|
|
54
|
+
typer.echo(f"错误: {e.message}")
|
|
55
|
+
raise typer.Exit(code=1) from None
|
|
56
|
+
|
|
57
|
+
if not result.items:
|
|
58
|
+
print("未查询到知识库")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
print_list(
|
|
62
|
+
("id", "name", "type", "knowledge_count"),
|
|
63
|
+
[
|
|
64
|
+
[it.id, it.name, it.type or "", it.knowledge_count if it.knowledge_count is not None else ""]
|
|
65
|
+
for it in result.items
|
|
66
|
+
],
|
|
67
|
+
)
|
|
68
|
+
print(f"total={result.total} page={result.page} page_size={result.page_size}")
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@kb_typer.command("detail", no_args_is_help=True)
|
|
72
|
+
def kb_detail_cmd(
|
|
73
|
+
kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
|
|
74
|
+
) -> None:
|
|
75
|
+
"""查看知识库详情。
|
|
76
|
+
|
|
77
|
+
Examples:
|
|
78
|
+
>>> data-ops-cli kb detail <kb_id>
|
|
79
|
+
"""
|
|
80
|
+
try:
|
|
81
|
+
kb = service_get_knowledge_base(kb_id)
|
|
82
|
+
except DataOpsError as e:
|
|
83
|
+
typer.echo(f"错误: {e.message}")
|
|
84
|
+
raise typer.Exit(code=1) from None
|
|
85
|
+
|
|
86
|
+
print_list(
|
|
87
|
+
("field", "value"),
|
|
88
|
+
[
|
|
89
|
+
["id", kb.id],
|
|
90
|
+
["name", kb.name],
|
|
91
|
+
["type", kb.type or ""],
|
|
92
|
+
["description", kb.description or ""],
|
|
93
|
+
["knowledge_count", kb.knowledge_count if kb.knowledge_count is not None else ""],
|
|
94
|
+
["chunk_count", kb.chunk_count if kb.chunk_count is not None else ""],
|
|
95
|
+
],
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
@doc_typer.command("list", no_args_is_help=True)
|
|
100
|
+
def doc_list_cmd(
|
|
101
|
+
kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
|
|
102
|
+
keyword: Annotated[
|
|
103
|
+
str | None,
|
|
104
|
+
typer.Option("--keyword", "-k", help="关键词"),
|
|
105
|
+
] = None,
|
|
106
|
+
page: Annotated[int, typer.Option("--page", help="页码", min=1)] = 1,
|
|
107
|
+
page_size: Annotated[int, typer.Option("--page-size", help="每页条数", min=1)] = 20,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""列出知识库文档。
|
|
110
|
+
|
|
111
|
+
Examples:
|
|
112
|
+
>>> data-ops-cli kb doc list <kb_id>
|
|
113
|
+
>>> data-ops-cli kb doc list <kb_id> --keyword "故障处理"
|
|
114
|
+
"""
|
|
115
|
+
try:
|
|
116
|
+
result = service_list_knowledge_documents(
|
|
117
|
+
kb_id, page=page, page_size=page_size, keyword=keyword
|
|
118
|
+
)
|
|
119
|
+
except DataOpsError as e:
|
|
120
|
+
typer.echo(f"错误: {e.message}")
|
|
121
|
+
raise typer.Exit(code=1) from None
|
|
122
|
+
|
|
123
|
+
if not result.items:
|
|
124
|
+
print("未查询到文档")
|
|
125
|
+
return
|
|
126
|
+
|
|
127
|
+
print_list(
|
|
128
|
+
("id", "title", "file_name", "parse_status"),
|
|
129
|
+
[
|
|
130
|
+
[
|
|
131
|
+
it.id,
|
|
132
|
+
it.title or "",
|
|
133
|
+
it.file_name or "",
|
|
134
|
+
it.parse_status or "",
|
|
135
|
+
]
|
|
136
|
+
for it in result.items
|
|
137
|
+
],
|
|
138
|
+
)
|
|
139
|
+
print(f"total={result.total} page={result.page} page_size={result.page_size}")
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
@doc_typer.command("upload", no_args_is_help=True)
|
|
143
|
+
def doc_upload_cmd(
|
|
144
|
+
kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
|
|
145
|
+
file: Annotated[Path, typer.Argument(help="本地文件路径", exists=True, readable=True)],
|
|
146
|
+
name: Annotated[
|
|
147
|
+
str | None,
|
|
148
|
+
typer.Option("--name", help="覆盖上传文件名"),
|
|
149
|
+
] = None,
|
|
150
|
+
) -> None:
|
|
151
|
+
"""上传文件到知识库。
|
|
152
|
+
|
|
153
|
+
Examples:
|
|
154
|
+
>>> data-ops-cli kb doc upload <kb_id> ./manual.pdf
|
|
155
|
+
"""
|
|
156
|
+
try:
|
|
157
|
+
doc = service_upload_knowledge_file(kb_id, file, file_name=name)
|
|
158
|
+
except (DataOpsError, FileNotFoundError) as e:
|
|
159
|
+
msg = getattr(e, "message", None) or str(e)
|
|
160
|
+
typer.echo(f"错误: {msg}")
|
|
161
|
+
raise typer.Exit(code=1) from None
|
|
162
|
+
|
|
163
|
+
print(f"上传成功: id={doc.id} title={doc.title or ''} file_name={doc.file_name or ''}")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
@kb_typer.command("search", no_args_is_help=True)
|
|
167
|
+
def search_cmd(
|
|
168
|
+
keyword: Annotated[str, typer.Argument(help="检索关键词")],
|
|
169
|
+
limit: Annotated[int, typer.Option("--limit", help="返回条数", min=1)] = 20,
|
|
170
|
+
offset: Annotated[int, typer.Option("--offset", help="偏移量", min=0)] = 0,
|
|
171
|
+
) -> None:
|
|
172
|
+
"""全局关键词检索知识内容。
|
|
173
|
+
|
|
174
|
+
Examples:
|
|
175
|
+
>>> data-ops-cli kb search "网络割接"
|
|
176
|
+
"""
|
|
177
|
+
try:
|
|
178
|
+
result = service_search_knowledge(keyword, limit=limit, offset=offset)
|
|
179
|
+
except DataOpsError as e:
|
|
180
|
+
typer.echo(f"错误: {e.message}")
|
|
181
|
+
raise typer.Exit(code=1) from None
|
|
182
|
+
|
|
183
|
+
if not result.items:
|
|
184
|
+
print("未检索到内容")
|
|
185
|
+
return
|
|
186
|
+
|
|
187
|
+
print_list(
|
|
188
|
+
("title", "kb_name", "score", "content"),
|
|
189
|
+
[
|
|
190
|
+
[
|
|
191
|
+
it.title or it.knowledge_title or "",
|
|
192
|
+
it.kb_name or "",
|
|
193
|
+
f"{it.score:.4f}" if it.score is not None else "",
|
|
194
|
+
(it.content or "")[:80],
|
|
195
|
+
]
|
|
196
|
+
for it in result.items
|
|
197
|
+
],
|
|
198
|
+
)
|
|
199
|
+
print(f"total={result.total}")
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
@page_typer.command("search", no_args_is_help=True)
|
|
203
|
+
def page_search_cmd(
|
|
204
|
+
kb_id: Annotated[str, typer.Argument(help="知识库 ID")],
|
|
205
|
+
query: Annotated[str, typer.Argument(help="检索词")],
|
|
206
|
+
limit: Annotated[int, typer.Option("--limit", help="返回条数", min=1)] = 20,
|
|
207
|
+
) -> None:
|
|
208
|
+
"""检索知识库 Wiki 页面。
|
|
209
|
+
|
|
210
|
+
Examples:
|
|
211
|
+
>>> data-ops-cli kb page search <kb_id> "运维手册"
|
|
212
|
+
"""
|
|
213
|
+
try:
|
|
214
|
+
pages = service_search_wiki_pages(kb_id, query, limit=limit)
|
|
215
|
+
except DataOpsError as e:
|
|
216
|
+
typer.echo(f"错误: {e.message}")
|
|
217
|
+
raise typer.Exit(code=1) from None
|
|
218
|
+
|
|
219
|
+
if not pages:
|
|
220
|
+
print("未检索到 Wiki 页面")
|
|
221
|
+
return
|
|
222
|
+
|
|
223
|
+
print_list(
|
|
224
|
+
("slug", "title", "page_type", "summary"),
|
|
225
|
+
[
|
|
226
|
+
[
|
|
227
|
+
it.slug,
|
|
228
|
+
it.title or "",
|
|
229
|
+
it.page_type or "",
|
|
230
|
+
(it.summary or "")[:60],
|
|
231
|
+
]
|
|
232
|
+
for it in pages
|
|
233
|
+
],
|
|
234
|
+
)
|