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,143 @@
|
|
|
1
|
+
"""卡片 SDK 模块.
|
|
2
|
+
|
|
3
|
+
封装卡片 (widget) 相关的 API 调用。
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic_core import to_json
|
|
9
|
+
|
|
10
|
+
from dataops_sdk.exceptions import DataOpsError
|
|
11
|
+
from dataops_sdk.schemas import MetaRelease, MetaType, PageParam, SqlQueryParam
|
|
12
|
+
from dataops_sdk.services.meta import release_list_get_by_code, release_list_list
|
|
13
|
+
from dataops_sdk.session import get_session
|
|
14
|
+
from dataops_sdk.utils import encrypt_body
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def service_list_widgets() -> list[MetaRelease]:
|
|
18
|
+
"""搜索卡片列表.
|
|
19
|
+
|
|
20
|
+
Returns:
|
|
21
|
+
MetaRelease 对象列表
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
DataOpsError: API 请求失败时
|
|
25
|
+
AuthenticationError: 未认证时
|
|
26
|
+
"""
|
|
27
|
+
return release_list_list(MetaType.WIDGET)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def service_get_widget_detail_by_code(code: str):
|
|
31
|
+
"""指定卡片详情.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
code: 卡片编码
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
卡片元数据模型。meta.data 为 dict,结构为:
|
|
38
|
+
subType: chart|metric|detail|topology|table|gis|statistic
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
Raises:
|
|
42
|
+
ValueError: 无效类型
|
|
43
|
+
NotFoundError: 卡片不存在
|
|
44
|
+
DataOpsError: API 请求失败
|
|
45
|
+
AuthenticationError: 未认证
|
|
46
|
+
"""
|
|
47
|
+
# 不传 _class,让 meta.data 保持为 dict,避免 schema 与后端实际数据不一致
|
|
48
|
+
return release_list_get_by_code(MetaType.WIDGET, code)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def service_get_widget_data_by_code(
|
|
52
|
+
code: str,
|
|
53
|
+
page_param: PageParam | None = None,
|
|
54
|
+
query_param: SqlQueryParam | None = None,
|
|
55
|
+
) -> list[dict[str, Any]]:
|
|
56
|
+
"""获取卡片实际数据(通过后端 /data/widget/findData 接口).
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
code: 卡片编码
|
|
60
|
+
page_param: 分页参数,默认为第一页,每页 20 条
|
|
61
|
+
query_param: 查询过滤条件
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
数据行列表(每行为字典)
|
|
65
|
+
|
|
66
|
+
Raises:
|
|
67
|
+
NotFoundError: 卡片不存在
|
|
68
|
+
DataOpsError: API 请求失败
|
|
69
|
+
AuthenticationError: 未认证
|
|
70
|
+
"""
|
|
71
|
+
# 先获取卡片元数据,以提取 tenantId / subSystem / prodCode
|
|
72
|
+
meta = service_get_widget_detail_by_code(code)
|
|
73
|
+
sub_system = getattr(meta, "subSystem", None) or "default"
|
|
74
|
+
|
|
75
|
+
if page_param is None:
|
|
76
|
+
page_param = PageParam(limit=20)
|
|
77
|
+
if query_param is None:
|
|
78
|
+
query_param = SqlQueryParam() # pyright: ignore[reportCallIssue]
|
|
79
|
+
|
|
80
|
+
body = {
|
|
81
|
+
"subSystem": sub_system,
|
|
82
|
+
"type": "widget",
|
|
83
|
+
"code": code,
|
|
84
|
+
"pageParam": page_param,
|
|
85
|
+
"queryParam": query_param,
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
with get_session(timeout=30) as session:
|
|
89
|
+
resp = session.post("/data/widget/findData", content=to_json(encrypt_body(body)))
|
|
90
|
+
resp_body = resp.json()
|
|
91
|
+
|
|
92
|
+
if not (200 <= resp.status_code < 300):
|
|
93
|
+
raise DataOpsError(f"请求失败({resp.status_code}): {resp_body}")
|
|
94
|
+
|
|
95
|
+
data = resp_body.get("data")
|
|
96
|
+
if data is None:
|
|
97
|
+
raise DataOpsError("查询异常")
|
|
98
|
+
return data
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def service_count_widget_by_code(
|
|
102
|
+
code: str,
|
|
103
|
+
query_param: SqlQueryParam | None = None,
|
|
104
|
+
) -> int:
|
|
105
|
+
"""获取卡片数据数量.
|
|
106
|
+
|
|
107
|
+
通过 `/data/widget/findData` 接口(请求体 `count=true`)。
|
|
108
|
+
|
|
109
|
+
Args:
|
|
110
|
+
code: 卡片编码
|
|
111
|
+
query_param: 查询条件
|
|
112
|
+
|
|
113
|
+
Returns:
|
|
114
|
+
卡片数据数量
|
|
115
|
+
|
|
116
|
+
Raises:
|
|
117
|
+
NotFoundError: 卡片不存在时
|
|
118
|
+
DataOpsError: API 请求失败时
|
|
119
|
+
AuthenticationError: 未认证时
|
|
120
|
+
"""
|
|
121
|
+
meta = service_get_widget_detail_by_code(code)
|
|
122
|
+
sub_system = getattr(meta, "subSystem", None) or "default"
|
|
123
|
+
|
|
124
|
+
if query_param is None:
|
|
125
|
+
query_param = SqlQueryParam() # pyright: ignore[reportCallIssue]
|
|
126
|
+
|
|
127
|
+
body = {
|
|
128
|
+
"subSystem": sub_system,
|
|
129
|
+
"type": MetaType.WIDGET,
|
|
130
|
+
"code": code,
|
|
131
|
+
"queryParam": query_param,
|
|
132
|
+
"count": True,
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
with get_session() as session:
|
|
136
|
+
resp = session.post("/data/widget/findData", content=to_json(encrypt_body(body)))
|
|
137
|
+
resp_body = resp.json()
|
|
138
|
+
if resp.status_code != 200:
|
|
139
|
+
raise DataOpsError(f"查询失败,{resp_body.get('msg')}", resp.status_code)
|
|
140
|
+
data = resp_body.get("data")
|
|
141
|
+
if data is None:
|
|
142
|
+
return 0
|
|
143
|
+
return int(data)
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from pydantic_core import to_json
|
|
2
|
+
|
|
3
|
+
from dataops_sdk.exceptions import AuthenticationError, DataOpsError
|
|
4
|
+
from dataops_sdk.schemas import DataWorkflow, MetaRelease, MetaType
|
|
5
|
+
from dataops_sdk.services.meta import (
|
|
6
|
+
release_list_get_by_code,
|
|
7
|
+
release_list_get_by_name,
|
|
8
|
+
release_list_list,
|
|
9
|
+
)
|
|
10
|
+
from dataops_sdk.session import get_session
|
|
11
|
+
from dataops_sdk.utils import encrypt_body, filter_nodes_by_pattern
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"service_get_workflow_by_name",
|
|
15
|
+
"service_get_workflow_by_code",
|
|
16
|
+
"service_list_workflows",
|
|
17
|
+
"service_start_workflow",
|
|
18
|
+
"service_start_workflow_async",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def service_list_workflows(query: str | None = None) -> list[MetaRelease]:
|
|
23
|
+
"""列出工作流(可选正则过滤).
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
query: 正则表达式,匹配 NAME / TITLE / REMARK / DIGEST
|
|
27
|
+
|
|
28
|
+
Returns:
|
|
29
|
+
MetaRelease 对象列表
|
|
30
|
+
|
|
31
|
+
Raises:
|
|
32
|
+
DataOpsError: API 请求失败时
|
|
33
|
+
AuthenticationError: 未认证时
|
|
34
|
+
"""
|
|
35
|
+
nodes = release_list_list(MetaType.WORKFLOW)
|
|
36
|
+
if query:
|
|
37
|
+
nodes = filter_nodes_by_pattern(nodes, query, include_digest=True)
|
|
38
|
+
return nodes
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def service_get_workflow_by_name(name: str):
|
|
42
|
+
return release_list_get_by_name(MetaType.WORKFLOW, name, _class=DataWorkflow)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def service_get_workflow_by_code(code: str):
|
|
46
|
+
return release_list_get_by_code(MetaType.WORKFLOW, code, _class=DataWorkflow)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _post_workflow(url: str, body: dict) -> dict:
|
|
50
|
+
"""通用工作流 POST 请求,返回 resp_body["data"].
|
|
51
|
+
|
|
52
|
+
Raises:
|
|
53
|
+
DataOpsError: API 返回错误时
|
|
54
|
+
AuthenticationError: 未认证或认证失败时
|
|
55
|
+
"""
|
|
56
|
+
try:
|
|
57
|
+
with get_session() as session:
|
|
58
|
+
resp = session.post(url, content=to_json(encrypt_body(body)))
|
|
59
|
+
resp_body = resp.json()
|
|
60
|
+
if resp.status_code == 401:
|
|
61
|
+
raise AuthenticationError("认证失败,请先登录")
|
|
62
|
+
if resp.status_code != 200:
|
|
63
|
+
raise DataOpsError(
|
|
64
|
+
resp_body.get("msg", f"HTTP {resp.status_code}"), resp.status_code
|
|
65
|
+
)
|
|
66
|
+
return resp_body.get("data") or {}
|
|
67
|
+
except SystemExit as e:
|
|
68
|
+
raise AuthenticationError("认证失败,请先登录") from e
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def service_start_workflow(code: str, params: dict | None = None) -> dict:
|
|
72
|
+
"""同步启动工作流实例.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
code: 工作流编码
|
|
76
|
+
params: 启动参数(透传给流程节点的入参)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
后端返回的启动结果(包含 flowInstCuid 等字段)
|
|
80
|
+
|
|
81
|
+
Raises:
|
|
82
|
+
DataOpsError: API 请求失败时
|
|
83
|
+
AuthenticationError: 未认证或认证失败时
|
|
84
|
+
"""
|
|
85
|
+
return _post_workflow("/workflow/start", {"code": code, "params": params or {}})
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def service_start_workflow_async(code: str, params: dict | None = None) -> dict:
|
|
89
|
+
"""异步启动工作流实例.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
code: 工作流编码
|
|
93
|
+
params: 启动参数(透传给流程节点的入参)
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
后端返回的启动结果(通常包含 taskId 等字段)
|
|
97
|
+
|
|
98
|
+
Raises:
|
|
99
|
+
DataOpsError: API 请求失败时
|
|
100
|
+
AuthenticationError: 未认证或认证失败时
|
|
101
|
+
"""
|
|
102
|
+
return _post_workflow("/workflow/startAsync", {"code": code, "params": params or {}})
|
dataops_sdk/session.py
ADDED
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from collections.abc import Mapping
|
|
3
|
+
from contextlib import contextmanager
|
|
4
|
+
from os import getenv
|
|
5
|
+
from typing import IO, BinaryIO
|
|
6
|
+
|
|
7
|
+
import httpx2
|
|
8
|
+
from dataops_sdk.constants import APPLICATION_FILE, USER_INFO_FILE
|
|
9
|
+
from dataops_sdk.exceptions import AuthenticationError, DataOpsError
|
|
10
|
+
from dataops_sdk.schemas.user import Application, UserInfo
|
|
11
|
+
from dataops_sdk.utils import get_work_dir
|
|
12
|
+
from pydantic import BaseModel
|
|
13
|
+
from pydantic_core import from_json, to_json
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DataOpsConfig(BaseModel):
|
|
17
|
+
"""可编程配置的 DataOps 会话参数.
|
|
18
|
+
|
|
19
|
+
优先级: 显式传入的参数 > 环境变量 > 默认值
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
base_url: str = getenv("DATA_OPS_BASE_URL", "http://172.168.63.85/bgw/data-ops-service")
|
|
23
|
+
username: str | None = None
|
|
24
|
+
password: str | None = None
|
|
25
|
+
token: str | None = None
|
|
26
|
+
token_type: str = "Bearer"
|
|
27
|
+
app_code: str | None = None
|
|
28
|
+
app_release: str = "1.0.0"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
# 模块级配置,可通过 dataops_sdk.configure() 修改
|
|
32
|
+
_config: DataOpsConfig = DataOpsConfig()
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def configure(
|
|
36
|
+
*,
|
|
37
|
+
base_url: str | None = None,
|
|
38
|
+
username: str | None = None,
|
|
39
|
+
password: str | None = None,
|
|
40
|
+
token: str | None = None,
|
|
41
|
+
token_type: str | None = None,
|
|
42
|
+
app_code: str | None = None,
|
|
43
|
+
app_release: str | None = None,
|
|
44
|
+
) -> DataOpsConfig:
|
|
45
|
+
"""配置 data-ops-cli 会话参数.
|
|
46
|
+
|
|
47
|
+
可用于替代登录或补充环境变量配置,使包可在无 CLI 登录的情况下使用。
|
|
48
|
+
|
|
49
|
+
Args:
|
|
50
|
+
base_url: API 基础 URL,覆盖环境变量 DATA_OPS_BASE_URL
|
|
51
|
+
username: DataOps 用户名
|
|
52
|
+
password: DataOps 密码(仅在使用内置 login 时需要)
|
|
53
|
+
token: 直接传入访问令牌
|
|
54
|
+
token_type: 令牌类型,默认 Bearer
|
|
55
|
+
app_code: 应用编码
|
|
56
|
+
app_release: 应用版本号
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
更新后的配置对象
|
|
60
|
+
"""
|
|
61
|
+
global _config
|
|
62
|
+
if base_url is not None:
|
|
63
|
+
_config.base_url = base_url
|
|
64
|
+
if username is not None:
|
|
65
|
+
_config.username = username
|
|
66
|
+
if password is not None:
|
|
67
|
+
_config.password = password
|
|
68
|
+
if token is not None:
|
|
69
|
+
_config.token = token
|
|
70
|
+
if token_type is not None:
|
|
71
|
+
_config.token_type = token_type
|
|
72
|
+
if app_code is not None:
|
|
73
|
+
_config.app_code = app_code
|
|
74
|
+
if app_release is not None:
|
|
75
|
+
_config.app_release = app_release
|
|
76
|
+
return _config
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_config() -> DataOpsConfig:
|
|
80
|
+
"""返回当前配置。"""
|
|
81
|
+
return _config
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def get_user_info_file_path() -> str:
|
|
85
|
+
"""返回用户信息文件路径。"""
|
|
86
|
+
return get_work_dir(USER_INFO_FILE)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def get_application_file_path() -> str:
|
|
90
|
+
"""返回应用信息文件路径。"""
|
|
91
|
+
return get_work_dir(APPLICATION_FILE)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _load_user_info_from_file() -> UserInfo | None:
|
|
95
|
+
"""从 ~/.dataops_cli/user_info.json 加载用户信息。"""
|
|
96
|
+
target_path = get_work_dir(USER_INFO_FILE)
|
|
97
|
+
if not target_path.exists():
|
|
98
|
+
return None
|
|
99
|
+
return UserInfo.model_validate_json(target_path.read_bytes())
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _load_application_from_file() -> Application | None:
|
|
103
|
+
"""从 ~/.dataops_cli/application.json 加载应用信息。"""
|
|
104
|
+
target_path = get_work_dir(APPLICATION_FILE)
|
|
105
|
+
if not target_path.exists():
|
|
106
|
+
return None
|
|
107
|
+
return Application.model_validate_json(target_path.read_bytes())
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def get_user_info(strict: bool = True) -> UserInfo | None:
|
|
111
|
+
"""获取本地存储的用户信息。
|
|
112
|
+
|
|
113
|
+
优先级: 显式配置的 token > user_info.json 文件
|
|
114
|
+
"""
|
|
115
|
+
if _config.token:
|
|
116
|
+
return UserInfo(
|
|
117
|
+
token_type=_config.token_type,
|
|
118
|
+
access_token=_config.token,
|
|
119
|
+
refresh_token="",
|
|
120
|
+
expires_in=0,
|
|
121
|
+
)
|
|
122
|
+
user_info = _load_user_info_from_file()
|
|
123
|
+
if user_info is None and strict:
|
|
124
|
+
print("using `data-ops-cli auth` to login first")
|
|
125
|
+
raise SystemExit(401)
|
|
126
|
+
return user_info
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def get_headers(strict: bool = True) -> Mapping[str, str]:
|
|
130
|
+
# 优先从环境变量获取
|
|
131
|
+
fmind_auth_headers = os.getenv("FMIND_AUTH_HEADERS", None)
|
|
132
|
+
if fmind_auth_headers is not None:
|
|
133
|
+
headers = from_json(fmind_auth_headers)
|
|
134
|
+
assert isinstance(headers, dict), "FMIND_AUTH_HEADERS must be a JSON object string"
|
|
135
|
+
return headers
|
|
136
|
+
# 其次从配置获取
|
|
137
|
+
user_info = get_user_info(strict=strict)
|
|
138
|
+
application = get_application(strict=strict)
|
|
139
|
+
headers = {}
|
|
140
|
+
if application is not None:
|
|
141
|
+
headers["X-App-Code"] = application.CODE
|
|
142
|
+
headers["X-App-Release"] = "1.0.0"
|
|
143
|
+
elif _config.app_code:
|
|
144
|
+
headers["X-App-Code"] = _config.app_code
|
|
145
|
+
headers["X-App-Release"] = _config.app_release
|
|
146
|
+
if not strict and user_info is None:
|
|
147
|
+
return headers
|
|
148
|
+
headers["Fmind-Auth"] = f"{user_info.token_type} {user_info.access_token}"
|
|
149
|
+
return headers
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def get_application(strict: bool = True) -> Application | None:
|
|
153
|
+
"""获取当前应用信息。
|
|
154
|
+
|
|
155
|
+
优先级: 显式配置的 app_code > application.json 文件
|
|
156
|
+
"""
|
|
157
|
+
if _config.app_code:
|
|
158
|
+
app_file = _load_application_from_file()
|
|
159
|
+
if app_file is not None:
|
|
160
|
+
return app_file
|
|
161
|
+
if strict:
|
|
162
|
+
print("using `data-ops-cli auth application` to set application first")
|
|
163
|
+
raise SystemExit(401)
|
|
164
|
+
return None
|
|
165
|
+
app = _load_application_from_file()
|
|
166
|
+
if app is None and strict:
|
|
167
|
+
print("using `data-ops-cli auth application` to set application first")
|
|
168
|
+
raise SystemExit(401)
|
|
169
|
+
return app
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def refresh_token(response: httpx2.Response):
|
|
173
|
+
if response.url.path.endswith("/oauth/token"):
|
|
174
|
+
return
|
|
175
|
+
if response.status_code == 200:
|
|
176
|
+
result: dict = response.json()
|
|
177
|
+
else:
|
|
178
|
+
result = dict()
|
|
179
|
+
need_auth = response.status_code == 401 and result.get("code", 400) == 401
|
|
180
|
+
if not need_auth:
|
|
181
|
+
return
|
|
182
|
+
url = "/oauth/token"
|
|
183
|
+
user_info = get_user_info(strict=True)
|
|
184
|
+
params = {
|
|
185
|
+
"refresh_token": user_info.refresh_token,
|
|
186
|
+
"grant_type": "refresh_token",
|
|
187
|
+
"scope": "all",
|
|
188
|
+
}
|
|
189
|
+
with get_session(False) as session:
|
|
190
|
+
resp = session.post(url, params=params)
|
|
191
|
+
response_body = resp.json()
|
|
192
|
+
|
|
193
|
+
if response.status_code == 200 and response_body.get("code") == 200:
|
|
194
|
+
user_info = UserInfo.model_validate(response_body["data"])
|
|
195
|
+
target_path = get_work_dir(USER_INFO_FILE)
|
|
196
|
+
target_path.write_bytes(to_json(user_info))
|
|
197
|
+
print("refresh token successfully")
|
|
198
|
+
return user_info
|
|
199
|
+
else:
|
|
200
|
+
raise AuthenticationError(
|
|
201
|
+
f"Login failed: {response_body.get('msg', f'HTTP {response.status_code}')}"
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@contextmanager
|
|
206
|
+
def get_session(strict: bool = True, timeout: int = 10):
|
|
207
|
+
"""获取请求 Session。
|
|
208
|
+
|
|
209
|
+
使用 httpx2 管理 HTTP 连接,base_url 指向 DataOps 服务。
|
|
210
|
+
"""
|
|
211
|
+
headers = get_headers(strict)
|
|
212
|
+
with httpx2.Client(
|
|
213
|
+
headers=headers,
|
|
214
|
+
base_url=_config.base_url,
|
|
215
|
+
verify=False,
|
|
216
|
+
timeout=timeout,
|
|
217
|
+
# event_hooks={"response": [refresh_token]},
|
|
218
|
+
) as session:
|
|
219
|
+
try:
|
|
220
|
+
yield session
|
|
221
|
+
finally:
|
|
222
|
+
session.close()
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@contextmanager
|
|
226
|
+
def post_multipart(
|
|
227
|
+
url: str,
|
|
228
|
+
*,
|
|
229
|
+
files: Mapping[str, tuple[str, IO[bytes] | BinaryIO] | IO[bytes] | BinaryIO],
|
|
230
|
+
data: Mapping[str, str] | None = None,
|
|
231
|
+
headers: Mapping[str, str] | None = None,
|
|
232
|
+
timeout: int = 30,
|
|
233
|
+
):
|
|
234
|
+
"""multipart/form-data POST 请求上下文管理器.
|
|
235
|
+
|
|
236
|
+
自动注入 base headers(含 ``Fmind-Auth``/``X-App-Code``),并做统一的异常映射:
|
|
237
|
+
- 401 → :class:`AuthenticationError`
|
|
238
|
+
- 非 200 → :class:`DataOpsError`
|
|
239
|
+
- 未登录抛出 ``SystemExit`` → :class:`AuthenticationError`
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
url: 相对 ``base_url`` 的路径,如 ``"/data/skill/import"``.
|
|
243
|
+
files: 表单文件字段,格式为 ``{"字段名": (filename, file_obj)}`` 或 ``{"字段名": file_obj}``.
|
|
244
|
+
data: 文本表单字段,如 ``{"subType": "llm"}``.
|
|
245
|
+
headers: 额外请求头,会与 base headers 合并.
|
|
246
|
+
timeout: 请求超时秒数,默认 30s(ZIP 等大文件可能需要更长时间).
|
|
247
|
+
|
|
248
|
+
Yields:
|
|
249
|
+
httpx2.Response: 已通过状态码校验的响应对象.
|
|
250
|
+
|
|
251
|
+
Raises:
|
|
252
|
+
AuthenticationError: 未登录或认证失效.
|
|
253
|
+
DataOpsError: API 返回非 200.
|
|
254
|
+
|
|
255
|
+
Examples:
|
|
256
|
+
>>> from pathlib import Path
|
|
257
|
+
>>> with open("skill.zip", "rb") as f:
|
|
258
|
+
... with post_multipart(
|
|
259
|
+
... "/data/skill/import",
|
|
260
|
+
... files={"file": ("skill.zip", f)},
|
|
261
|
+
... data={"subType": "llm"},
|
|
262
|
+
... ) as resp:
|
|
263
|
+
... payload = resp.json()
|
|
264
|
+
"""
|
|
265
|
+
extra_headers: dict[str, str] = dict(get_headers()) # type: ignore[arg-type]
|
|
266
|
+
if headers:
|
|
267
|
+
extra_headers.update(dict(headers))
|
|
268
|
+
try:
|
|
269
|
+
with httpx2.Client(
|
|
270
|
+
headers=extra_headers,
|
|
271
|
+
base_url=_config.base_url,
|
|
272
|
+
verify=False,
|
|
273
|
+
timeout=timeout,
|
|
274
|
+
) as client:
|
|
275
|
+
try:
|
|
276
|
+
resp = client.post(url, files=files, data=data)
|
|
277
|
+
if resp.status_code == 401:
|
|
278
|
+
raise AuthenticationError("认证失败,请先登录")
|
|
279
|
+
if resp.status_code != 200:
|
|
280
|
+
body: dict = {}
|
|
281
|
+
try:
|
|
282
|
+
body = resp.json()
|
|
283
|
+
except Exception:
|
|
284
|
+
pass
|
|
285
|
+
raise DataOpsError(
|
|
286
|
+
body.get("msg", f"HTTP {resp.status_code}"), resp.status_code
|
|
287
|
+
)
|
|
288
|
+
yield resp
|
|
289
|
+
finally:
|
|
290
|
+
client.close()
|
|
291
|
+
except SystemExit as e:
|
|
292
|
+
raise AuthenticationError("认证失败,请先登录") from e
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""TipTap module for converting between TipTap JSON and Markdown."""
|
|
2
|
+
|
|
3
|
+
from dataops_sdk.tiptap.builder import TipTapBuilder
|
|
4
|
+
from dataops_sdk.tiptap.constants import (
|
|
5
|
+
AttrTypes,
|
|
6
|
+
IndentStyle,
|
|
7
|
+
Markdown,
|
|
8
|
+
MarkTypes,
|
|
9
|
+
NodeTypes,
|
|
10
|
+
Style,
|
|
11
|
+
TableStyle,
|
|
12
|
+
)
|
|
13
|
+
from dataops_sdk.tiptap.models import DocNode, Mark, Node, TextNode
|
|
14
|
+
from dataops_sdk.tiptap.tool import TipTapTool
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"TipTapTool",
|
|
18
|
+
"TipTapBuilder",
|
|
19
|
+
"AttrTypes",
|
|
20
|
+
"IndentStyle",
|
|
21
|
+
"Markdown",
|
|
22
|
+
"MarkTypes",
|
|
23
|
+
"NodeTypes",
|
|
24
|
+
"Style",
|
|
25
|
+
"TableStyle",
|
|
26
|
+
"DocNode",
|
|
27
|
+
"Mark",
|
|
28
|
+
"Node",
|
|
29
|
+
"TextNode",
|
|
30
|
+
]
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""TipTapBuilder factory for creating TipTapTool instances."""
|
|
2
|
+
|
|
3
|
+
from dataops_sdk.tiptap import constants
|
|
4
|
+
from dataops_sdk.tiptap.tool import TipTapTool
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TipTapBuilder:
|
|
8
|
+
"""Builder for TipTapTool instances.
|
|
9
|
+
|
|
10
|
+
Supports creating TipTapTool with default or custom configurations.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def build(
|
|
15
|
+
indent_style: str = constants.Style.INDENT_STYLE_SPACE,
|
|
16
|
+
indent_size: int = constants.Style.DEFAULT_INDENT_SIZE,
|
|
17
|
+
table_style: str = constants.Style.DEFAULT_TABLE_STYLE,
|
|
18
|
+
) -> TipTapTool:
|
|
19
|
+
"""Build with configuration.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
indent_style: Indent style (space or tab)
|
|
23
|
+
indent_size: Number of indent characters
|
|
24
|
+
table_style: Table rendering style (markdown or html)
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
TipTapTool instance with settings
|
|
28
|
+
"""
|
|
29
|
+
return TipTapTool(
|
|
30
|
+
indent_style=indent_style,
|
|
31
|
+
indent_size=indent_size,
|
|
32
|
+
table_style=table_style,
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@staticmethod
|
|
36
|
+
def create_default() -> TipTapTool:
|
|
37
|
+
"""Create with default configuration.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
TipTapTool instance with default settings
|
|
41
|
+
"""
|
|
42
|
+
return TipTapTool()
|
|
43
|
+
|
|
44
|
+
@staticmethod
|
|
45
|
+
def create_with_table_style(table_style: str) -> TipTapTool:
|
|
46
|
+
"""Create with custom table style.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
table_style: Table rendering style (markdown or html)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
TipTapTool instance with custom table style
|
|
53
|
+
"""
|
|
54
|
+
return TipTapTool(
|
|
55
|
+
indent_style=constants.Style.INDENT_STYLE_SPACE,
|
|
56
|
+
indent_size=constants.Style.DEFAULT_INDENT_SIZE,
|
|
57
|
+
table_style=table_style,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@staticmethod
|
|
61
|
+
def create_with_indent_config(indent_style: str, indent_size: int) -> TipTapTool:
|
|
62
|
+
"""Create with custom indent configuration.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
indent_style: Indent style (space or tab)
|
|
66
|
+
indent_size: Number of indent characters
|
|
67
|
+
|
|
68
|
+
Returns:
|
|
69
|
+
TipTapTool instance with custom indent settings
|
|
70
|
+
"""
|
|
71
|
+
return TipTapTool(
|
|
72
|
+
indent_style=indent_style,
|
|
73
|
+
indent_size=indent_size,
|
|
74
|
+
table_style=constants.Style.DEFAULT_TABLE_STYLE,
|
|
75
|
+
)
|