data-ops-cli 0.4.0__tar.gz → 0.4.2__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.
Files changed (39) hide show
  1. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/PKG-INFO +2 -2
  2. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/pyproject.toml +1 -1
  3. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/data_ops_cli.egg-info/PKG-INFO +2 -2
  4. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/data_ops_cli.egg-info/SOURCES.txt +2 -1
  5. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/data_ops_cli.egg-info/requires.txt +1 -1
  6. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/__init__.py +1 -1
  7. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/auth.py +30 -20
  8. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/dataset.py +141 -3
  9. data_ops_cli-0.4.2/src/dataops_cli/cli/dict_.py +219 -0
  10. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/metrics.py +68 -4
  11. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/ref.py +5 -20
  12. data_ops_cli-0.4.2/src/dataops_cli/cli/report.py +142 -0
  13. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/skill.py +3 -2
  14. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/table.py +162 -7
  15. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/utils.py +73 -1
  16. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/widget.py +40 -5
  17. data_ops_cli-0.4.2/tests/test_table_relation.py +450 -0
  18. data_ops_cli-0.4.0/src/dataops_cli/cli/dict_.py +0 -101
  19. data_ops_cli-0.4.0/src/dataops_cli/cli/report.py +0 -81
  20. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/README.md +0 -0
  21. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/setup.cfg +0 -0
  22. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/data_ops_cli.egg-info/dependency_links.txt +0 -0
  23. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/data_ops_cli.egg-info/entry_points.txt +0 -0
  24. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/data_ops_cli.egg-info/top_level.txt +0 -0
  25. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/_compat.py +0 -0
  26. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/__init__.py +0 -0
  27. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/alarm.py +0 -0
  28. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/api.py +0 -0
  29. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/data_table.py +0 -0
  30. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/datasource.py +0 -0
  31. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/factory.py +0 -0
  32. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/hazard.py +0 -0
  33. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/cli/workflow.py +0 -0
  34. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/exceptions.py +0 -0
  35. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/runner.py +0 -0
  36. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/schems.py +0 -0
  37. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/sdk.py +0 -0
  38. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/session.py +0 -0
  39. {data_ops_cli-0.4.0 → data_ops_cli-0.4.2}/src/dataops_cli/utils.py +0 -0
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-ops-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Command-line wrapper around dataops_sdk for the DataOps platform.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: dataops-sdk[runner]>=0.4.0
7
+ Requires-Dist: dataops-sdk[runner]>=0.4.1
8
8
  Requires-Dist: typer
9
9
  Requires-Dist: rich
10
10
  Provides-Extra: dev
@@ -9,7 +9,7 @@ description = "Command-line wrapper around dataops_sdk for the DataOps platform.
9
9
  requires-python = ">=3.11"
10
10
  readme = "README.md"
11
11
  dependencies = [
12
- "dataops-sdk[runner]>=0.4.0",
12
+ "dataops-sdk[runner]>=0.4.1",
13
13
  "typer",
14
14
  "rich",
15
15
  ]
@@ -1,10 +1,10 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: data-ops-cli
3
- Version: 0.4.0
3
+ Version: 0.4.2
4
4
  Summary: Command-line wrapper around dataops_sdk for the DataOps platform.
5
5
  Requires-Python: >=3.11
6
6
  Description-Content-Type: text/markdown
7
- Requires-Dist: dataops-sdk[runner]>=0.4.0
7
+ Requires-Dist: dataops-sdk[runner]>=0.4.1
8
8
  Requires-Dist: typer
9
9
  Requires-Dist: rich
10
10
  Provides-Extra: dev
@@ -31,4 +31,5 @@ src/dataops_cli/cli/skill.py
31
31
  src/dataops_cli/cli/table.py
32
32
  src/dataops_cli/cli/utils.py
33
33
  src/dataops_cli/cli/widget.py
34
- src/dataops_cli/cli/workflow.py
34
+ src/dataops_cli/cli/workflow.py
35
+ tests/test_table_relation.py
@@ -1,4 +1,4 @@
1
- dataops-sdk[runner]>=0.4.0
1
+ dataops-sdk[runner]>=0.4.1
2
2
  typer
3
3
  rich
4
4
 
@@ -14,4 +14,4 @@ groups command modules under :mod:`dataops_cli.cli` and re-exports the package
14
14
  removed in ``dataops_cli`` 1.0.
15
15
  """
16
16
 
17
- __version__ = "0.4.0"
17
+ __version__ = "0.4.2"
@@ -9,14 +9,14 @@
9
9
  from typing import Annotated
10
10
 
11
11
  import typer
12
- from dataops_sdk.constants import USER_INFO_FILE
12
+ from dataops_cli.cli.utils import print_list
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
15
16
  from dataops_sdk.services.auth import (
16
17
  service_get_current_application,
17
18
  service_list_applications,
18
19
  service_login,
19
- service_logout,
20
20
  service_set_application,
21
21
  )
22
22
  from dataops_sdk.services.datasource import (
@@ -26,13 +26,20 @@ from dataops_sdk.utils import get_work_dir
26
26
  from rich.console import Console
27
27
  from rich.table import Table
28
28
 
29
- from dataops_cli.cli.utils import print_list
30
-
31
29
  auth_typer = typer.Typer(name="auth", help="认证相关命令", no_args_is_help=True)
32
30
  application_typer = typer.Typer(name="application", help="应用相关命令", no_args_is_help=True)
33
31
  auth_typer.add_typer(application_typer)
34
32
 
35
33
 
34
+ def _require_login() -> None:
35
+ """未登录时引导登录并退出。"""
36
+ if not get_work_dir(USER_INFO_FILE).exists():
37
+ typer.echo("未登录,请先执行登录:")
38
+ typer.echo(" data-ops-cli auth login <用户名> <密码>")
39
+ typer.echo("登录后执行 data-ops-cli auth application set <应用code> 设置应用")
40
+ raise typer.Exit(code=401)
41
+
42
+
36
43
  @auth_typer.command("status")
37
44
  def auth_status() -> None:
38
45
  """显示当前认证状态。
@@ -43,22 +50,22 @@ def auth_status() -> None:
43
50
  如果已登录但未设置应用,提示用户设置应用;
44
51
  否则同时输出应用关联的主/隐患数据源信息。
45
52
  """
46
- target_path = get_work_dir(USER_INFO_FILE)
47
- if not target_path.exists():
48
- print("未登录")
49
- else:
50
- user_info = UserInfo.model_validate_json(target_path.read_bytes())
53
+ _require_login()
54
+ user_info = UserInfo.model_validate_json(get_work_dir(USER_INFO_FILE).read_bytes())
51
55
  try:
52
- # application
53
56
  application = service_get_current_application()
54
57
  datasource_config = service_get_main_datasource_config_by_application()
55
58
  except AuthenticationError:
56
59
  print(f"{user_info.true_name}({user_info.user_name})登录过期,请重新登录")
57
60
  else:
58
61
  if application is None:
59
- print(f"{user_info.true_name}({user_info.user_name})已登录, 应用未设置")
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)
60
67
  else:
61
- print(
68
+ typer.echo(
62
69
  f"{user_info.true_name}({user_info.user_name})已登录, 应用设置为{application.LABEL_CN}({application.CODE})"
63
70
  )
64
71
 
@@ -120,11 +127,11 @@ def logout() -> None:
120
127
  Examples:
121
128
  >>> data-ops-cli auth logout
122
129
  """
123
- ret = service_logout()
124
- if ret:
125
- print("登出成功")
126
- else:
127
- print("登出失败: 未登录")
130
+ for filename in (USER_INFO_FILE, APPLICATION_FILE):
131
+ path = get_work_dir(filename)
132
+ if path.exists() and path.is_file():
133
+ path.unlink()
134
+ print("登出成功,已清除本地登录信息")
128
135
 
129
136
 
130
137
  @application_typer.command("list")
@@ -146,6 +153,7 @@ def application_list(
146
153
  通过关键字过滤应用
147
154
  >>> data-ops-cli auth application list -q myapp
148
155
  """
156
+ _require_login()
149
157
  try:
150
158
  applications = service_list_applications(query)
151
159
  except Exception as e:
@@ -175,11 +183,12 @@ def application_set(code: Annotated[str, typer.Argument(help="应用编码")]) -
175
183
  设置当前应用
176
184
  >>> data-ops-cli auth application set myapp_code
177
185
  """
186
+ _require_login()
178
187
  try:
179
188
  application = service_set_application(code)
180
- except ValueError as e:
181
- typer.echo(f"错误: {e}")
182
- raise typer.Exit(code=1) from None
189
+ except ValueError:
190
+ typer.echo("输入的code值错误,请重新输入!")
191
+ raise typer.Exit(code=1)
183
192
 
184
193
  table = Table("field", "value", title="应用信息", show_header=False)
185
194
  table.add_row("code", application.CODE)
@@ -197,6 +206,7 @@ def application_info() -> None:
197
206
  查看当前已设置应用的详细信息,包括编码、标题和备注。
198
207
  如果尚未设置应用,提示用户使用 ``auth application set`` 命令进行设置。
199
208
  """
209
+ _require_login()
200
210
  application = service_get_current_application()
201
211
  if application is None:
202
212
  print("未设置应用, 执行 `data-ops-cli auth application set <code>` 进行设置应用.")
@@ -1,24 +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,
16
+ release_list_update,
12
17
  service_count_dataset_by_code,
13
18
  service_get_dataset_data_by_code,
14
19
  service_get_dataset_detail_by_code,
15
20
  service_list_datasets,
16
21
  )
17
- from dataops_sdk.utils import filter_nodes_by_pattern
22
+ from dataops_sdk.session import get_session
23
+ from dataops_sdk.utils import encrypt_body, filter_nodes_by_pattern
18
24
 
19
25
  from dataops_cli.cli.utils import print_list, print_table_column_detail
20
26
 
27
+ # 数据集 rawData 中承载 SQL 定义的字段名(subType=sql 的数据集均有此字段)。
28
+ SQL_FIELD = "text"
29
+
30
+
21
31
  dataset_typer = typer.Typer(name="dataset", help="Manage dataset.", no_args_is_help=True)
32
+ update_typer = typer.Typer(name="update", help="变更", no_args_is_help=True)
33
+ dataset_typer.add_typer(update_typer)
34
+ column_typer = typer.Typer(name="column", help="列变更", no_args_is_help=True)
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)
22
38
 
23
39
 
24
40
  @dataset_typer.command("list")
@@ -195,3 +211,125 @@ def get_dataset_data(
195
211
  headers = list(data[0].keys())
196
212
  rows = (tuple(row.get(header) for header in headers) for row in data)
197
213
  print_list(headers, rows)
214
+
215
+
216
+ @column_typer.command("base", no_args_is_help=True)
217
+ def update_dataset_column(
218
+ code: Annotated[str, typer.Argument(help="数据集编码")],
219
+ column_name: Annotated[str, typer.Argument(help="列名")],
220
+ title: Annotated[str | None, typer.Option(help="中文名称")] = None,
221
+ pk: Annotated[bool | None, typer.Option(help="主键")] = None,
222
+ nullable: Annotated[bool | None, typer.Option(help="是否可为空")] = None,
223
+ labelKey: Annotated[bool | None, typer.Option(help="名称键")] = None,
224
+ remark: Annotated[str | None, typer.Option(help="备注")] = None,
225
+ ) -> None:
226
+ """修改数据集的列基础配置."""
227
+ updates = {
228
+ attr: value
229
+ for attr, value in {
230
+ "title": title,
231
+ "pk": pk,
232
+ "nullable": nullable,
233
+ "labelKey": labelKey,
234
+ "remark": remark,
235
+ }.items()
236
+ if value is not None
237
+ }
238
+ if not updates:
239
+ typer.echo("请输入要修改的列配置", err=True)
240
+ raise typer.Exit(code=1)
241
+ try:
242
+ meta = service_get_dataset_detail_by_code(code)
243
+ assert meta.data, f"数据集 {code} 不存在"
244
+ column_item = next((c for c in meta.data.columns if c.name == column_name), None)
245
+ if column_item is None:
246
+ typer.echo(f"数据集 {code} 中不存在字段 {column_name}")
247
+ raise typer.Exit(code=1)
248
+ for attr, value in updates.items():
249
+ setattr(column_item, attr, value)
250
+ release_list_update(meta)
251
+ except DataOpsError as e:
252
+ typer.echo(f"错误: {e.message}")
253
+ raise typer.Exit(code=1) from None
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} 完成")
@@ -9,12 +9,12 @@
9
9
  from typing import Annotated
10
10
 
11
11
  import typer
12
- from dataops_sdk.schemas.enums import MetaType
13
- from dataops_sdk.schemas.query import SqlQueryParam
14
- from dataops_sdk.services.meta import (
15
- DataOpsError,
12
+ from dataops_sdk.exceptions import DataOpsError
13
+ from dataops_sdk.schemas import MetaType, PageParam, SqlFilter, SqlQueryParam
14
+ from dataops_sdk.services import (
16
15
  release_list_get_by_code,
17
16
  release_list_list,
17
+ service_get_metrics_data_by_code,
18
18
  )
19
19
  from dataops_sdk.utils import filter_nodes_by_pattern
20
20
  from rich.console import Console
@@ -175,3 +175,67 @@ def get_metrics_detail(
175
175
  style.get("color", ""),
176
176
  )
177
177
  console.print(level_table)
178
+
179
+
180
+ @metrics_typer.command("data", no_args_is_help=True)
181
+ def get_metrics_data(
182
+ code: Annotated[str, typer.Argument(help="指标编码")],
183
+ page_num: Annotated[int, typer.Option("-n", "--page-num", help="页码")] = 0,
184
+ page_size: Annotated[int, typer.Option("-s", "--page-size", help="每页大小")] = 20,
185
+ filters: Annotated[
186
+ list[SqlFilter] | None,
187
+ typer.Option(
188
+ "--condition",
189
+ "-c",
190
+ parser=SqlFilter.from_str,
191
+ help='查询条件(可多次传入),JSON 格式 {"name":"字段名","value":值[,"expression":"eq"]},expression 缺省 eq',
192
+ ),
193
+ ] = None,
194
+ dimensions: Annotated[
195
+ list[str] | None,
196
+ typer.Option(
197
+ "--dimension",
198
+ "-d",
199
+ help="维度字段名",
200
+ ),
201
+ ] = None,
202
+ ) -> None:
203
+ """获取指标实际数据。
204
+
205
+ 通过指标编码查询指标维度下的实际数据行,支持分页与查询条件过滤。
206
+
207
+ Args:
208
+ code: 指标编码。
209
+ page_num: 页码,默认 0。
210
+ page_size: 每页大小,默认 20。
211
+ filters: 查询条件,JSON 格式 ``{"name": 字段名, "value": 值[, "expression": 表达式]}``。
212
+ 可多次传入,多个条件按 AND 组合;``expression`` 缺省为 ``eq``,可选
213
+ ``eq/ne/gt/ge/lt/le/in/notin/like/notlike/between/notbetween/isnull/isnotnull``。
214
+
215
+ Examples:
216
+ 通过指标编码获取指标数据(默认返回前20条)
217
+ >>> data-ops-cli metrics data 1777544300561_98429
218
+
219
+ 从第2页开始,每页50条
220
+ >>> data-ops-cli metrics data 1777544300561_98429 --page-num 2 --page-size 50
221
+
222
+ 带查询条件过滤(d_cycle_time 等于 2026-04-18)
223
+ >>> data-ops-cli metrics data 1777544300561_98429 -c '{"name":"d_cycle_time","value":"2026-04-18"}'
224
+ """
225
+ pp = PageParam(start=page_num * page_size, limit=page_size)
226
+
227
+ qp = SqlQueryParam()
228
+ if filters:
229
+ qp.add(*(filter_item.to_condition() for filter_item in filters))
230
+ try:
231
+ data = service_get_metrics_data_by_code(code, pp, qp, dimensions)
232
+ except DataOpsError as e:
233
+ typer.echo(f"错误: {e.message}")
234
+ raise typer.Exit(code=1) from None
235
+ if not data:
236
+ typer.echo("未查询到数据", err=True)
237
+ return
238
+ # 使用第一行字段顺序作为表头,保证每行数据与表头位置对应。
239
+ headers = list(data[0].keys())
240
+ rows = (tuple(row.get(header) for header in headers) for row in data)
241
+ print_list(headers, rows)