fiuai-sdk-python 0.11.9__tar.gz → 0.13.0__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.
- fiuai_sdk_python-0.13.0/CHANGELOG.md +61 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/PKG-INFO +1 -1
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/pyproject.toml +1 -1
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/__init__.py +15 -1
- fiuai_sdk_python-0.13.0/src/fiuai_sdk_python/_resp_helpers.py +163 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/client.py +187 -65
- fiuai_sdk_python-0.13.0/src/fiuai_sdk_python/error_code.py +145 -0
- fiuai_sdk_python-0.13.0/src/fiuai_sdk_python/error_parsers.py +122 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/cache/redis_manager.py +14 -3
- fiuai_sdk_python-0.13.0/src/fiuai_sdk_python/resp.py +494 -0
- fiuai_sdk_python-0.11.9/CHANGELOG.md +0 -37
- fiuai_sdk_python-0.11.9/src/fiuai_sdk_python/resp.py +0 -289
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/.gitignore +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/LICENSE +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/README.md +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/auth/__init__.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/auth/context_mgr.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/auth/header.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/auth/helper.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/auth/type.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/bank.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/company.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/config.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/const.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/context.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/datatype.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/doctype.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/error.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/examples/fastapi_integration.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/http/__init__.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/http/client.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/item.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/perm.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/cache/__init__.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/cache/cache_client.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/cache/circuit_breaker.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/cache/decorator.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/cache/types.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/scheduler/__init__.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/pkg/scheduler/runner.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/profile.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/setup.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/type.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/util.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/utils/__init__.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/utils/ids.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/utils/logger.py +0 -0
- {fiuai_sdk_python-0.11.9 → fiuai_sdk_python-0.13.0}/src/fiuai_sdk_python/utils/text.py +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.13.0] - 2026-05-13
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `ApiError` 字段 (`code` / `type` / `message` / `detail` / `traceback`) 增加 `field_validator(mode="before")`, 对 frappe 偶发返回的 `list` / `dict` / `None` 类型自动规范化为 `str`, 修复 `ApiError.message=['Contract already exists']` 等 list 形态触发 Pydantic 校验失败导致 `parse_response` 整体崩溃的问题
|
|
12
|
+
- `_normalize_error_dict` 入口对 `type` / `message` / `display_message` / `title` / `code` / `context` 同步用 `_to_text` 兜底规范化, 避免 `_MANDATORY_RE.match(...)` 在拿到非 str 时抛 `TypeError`
|
|
13
|
+
- `parse_response` 加最外层 try/except 兜底, 任何未预期异常都返回结构化 `ApiResponse(error.code=SERVER_ERROR)`, 兑现"绝不抛异常"契约
|
|
14
|
+
- `FiuaiSDK.get_user_profile_info` 解析失败分支错误地把 `str` 赋给 `ApiError` 字段、给没有 setter 的 deprecated property 赋值, 导致二次崩溃, 现改为构造 `ApiError(code=INTERNAL_VALIDATION_ERROR)` 写入 `error/errors`
|
|
15
|
+
- `__version__` 与 `pyproject.toml` 版本号不一致 (`0.11.6` vs `0.12.0`) 统一为 `0.13.0`
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
- `FiuaiSDK._safe_request` 内部统一封装所有 `httpx` 调用, 捕获 `TimeoutException` / `RequestError` / 其他异常, 统一返回 `ApiResponse(error.code=NETWORK_ERROR)` 或 `SERVER_ERROR`, SDK 调用方不再需要捕获 httpx 异常
|
|
19
|
+
- `resp._to_text` 公共兜底 helper, 把任意 (list / dict / None / 其他) 安全规范化为 str
|
|
20
|
+
|
|
21
|
+
## [Unreleased]
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- 新增结构化错误处理: `ApiError` model + `FiuaiErrorCode` 枚举
|
|
25
|
+
- `ApiError` 字段: `code` / `type` / `message` / `detail` / `context` / `http_status_code` / `traceback`
|
|
26
|
+
- `FiuaiErrorCode` 提供稳定错误码枚举 (跨语言/跨版本不变), 与 frappe 二开侧 codes.py 对齐
|
|
27
|
+
- `ApiResponse` 新增字段:
|
|
28
|
+
- `error: ApiError | None` - 主错误 (errors[0] 规整后)
|
|
29
|
+
- `errors: list[ApiError]` - 所有错误项
|
|
30
|
+
- `warnings: list[str]` - 非错误的提示文案
|
|
31
|
+
- `raw: dict` - 原始 frappe response (调试用)
|
|
32
|
+
- SDK 内置兜底解析器 `error_parsers.py`, 服务端没补 `errors[].context` 时自动从 frappe 异常字符串提取 doctype/name/fields
|
|
33
|
+
- 网络层异常 (`response=None` / 非 JSON / 非 dict body) 也统一走 `ApiError`, code 为 `NETWORK_ERROR` / `API_INVALID_JSON` / `API_NO_RESPONSE`
|
|
34
|
+
- frappe `messages[]` 中红色友好文案 (启发式: indicator=red / title=错误 / 文本以"错误:"开头 / raise_exception 标记) 自动合并到主 `ApiError.message`
|
|
35
|
+
|
|
36
|
+
### Changed
|
|
37
|
+
- `parse_response` 重写, 返回的 `ApiResponse` 始终包含结构化 `error` 字段, 一定不会抛异常
|
|
38
|
+
- `_is_api_success` 修复: `data is None` 不再判失败, `delete` / `cancel` 等正常返回 None 的接口不会被误判
|
|
39
|
+
|
|
40
|
+
### Deprecated
|
|
41
|
+
- `ApiResponse.error_code` / `ApiResponse.error_message` 改为派生 property, 访问触发 `DeprecationWarning`
|
|
42
|
+
- 迁移路径:
|
|
43
|
+
- `resp.error_message[0]` → `resp.error.message`
|
|
44
|
+
- `resp.error_code[0]` → `resp.error.code`
|
|
45
|
+
- 下个 major 版本将移除
|
|
46
|
+
|
|
47
|
+
### Removed
|
|
48
|
+
- N/A
|
|
49
|
+
|
|
50
|
+
### Fixed
|
|
51
|
+
- `parse_response` 在 HTTP 502/504 等代理层 HTML 错误时不再 silent fallback, 返回 code=`API_INVALID_JSON` 的结构化 ApiError
|
|
52
|
+
- `_extract_error_info` 中无意义的 try/except 包装移除
|
|
53
|
+
|
|
54
|
+
### Security
|
|
55
|
+
- N/A
|
|
56
|
+
|
|
57
|
+
## [0.1.0] - 2025-01-01
|
|
58
|
+
|
|
59
|
+
### Added
|
|
60
|
+
- Initial release
|
|
61
|
+
- Basic SDK structure and functionality
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fiuai_sdk_python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.13.0
|
|
4
4
|
Summary: FiuAI Python SDK - 企业级AI服务集成开发工具包
|
|
5
5
|
Project-URL: Homepage, https://github.com/fiuai/fiuai-sdk-python
|
|
6
6
|
Project-URL: Documentation, https://github.com/fiuai/fiuai-sdk-python#readme
|
|
@@ -10,8 +10,15 @@ from .context import (
|
|
|
10
10
|
is_current_context_valid,
|
|
11
11
|
get_trace_id,
|
|
12
12
|
)
|
|
13
|
+
from .error_code import (
|
|
14
|
+
EXCEPTION_TO_CODE,
|
|
15
|
+
FiuaiErrorCode,
|
|
16
|
+
exception_to_code,
|
|
17
|
+
status_code_to_code,
|
|
18
|
+
)
|
|
19
|
+
from .resp import ApiError, ApiResponse, parse_response
|
|
13
20
|
|
|
14
|
-
__version__ = "0.
|
|
21
|
+
__version__ = "0.13.0"
|
|
15
22
|
|
|
16
23
|
__all__ = [
|
|
17
24
|
'FiuaiSDK',
|
|
@@ -25,4 +32,11 @@ __all__ = [
|
|
|
25
32
|
'get_current_missing_fields',
|
|
26
33
|
'is_current_context_valid',
|
|
27
34
|
'get_trace_id',
|
|
35
|
+
'ApiError',
|
|
36
|
+
'ApiResponse',
|
|
37
|
+
'parse_response',
|
|
38
|
+
'FiuaiErrorCode',
|
|
39
|
+
'EXCEPTION_TO_CODE',
|
|
40
|
+
'exception_to_code',
|
|
41
|
+
'status_code_to_code',
|
|
28
42
|
]
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuai_sdk_python
|
|
3
|
+
# Created Date: 2026-05-09
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Email: lmlala@aliyun.com
|
|
6
|
+
# Copyright (c) 2025 FiuAI
|
|
7
|
+
|
|
8
|
+
"""
|
|
9
|
+
resp.py 的内部 helper 集合.
|
|
10
|
+
|
|
11
|
+
不是公开 API, 不要从外部直接 import. 拆出来仅为了控制 resp.py 行数 (单文件 < 400 行规约).
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import logging
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from httpx import Response
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
# --------------------------------------------------------------------------
|
|
23
|
+
# 响应体读取 (用于 JSON 解析失败时的日志/预览)
|
|
24
|
+
# --------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_response_body_preview(response: Response, max_chars: int = 300) -> str:
|
|
28
|
+
"""安全读取响应体前若干字符, 用于错误信息预览. 兼容编码问题及中文."""
|
|
29
|
+
try:
|
|
30
|
+
raw = response.content
|
|
31
|
+
if not raw:
|
|
32
|
+
return ""
|
|
33
|
+
text = raw.decode("utf-8", errors="replace").strip()
|
|
34
|
+
if len(text) > max_chars:
|
|
35
|
+
text = text[:max_chars] + "..."
|
|
36
|
+
return text
|
|
37
|
+
except Exception:
|
|
38
|
+
return ""
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def get_response_body_for_log(response: Response, max_chars: int = 2000) -> str:
|
|
42
|
+
"""安全读取完整响应体用于日志, 避免编码异常, 支持中文."""
|
|
43
|
+
try:
|
|
44
|
+
raw = response.content
|
|
45
|
+
if not raw:
|
|
46
|
+
return "(empty)"
|
|
47
|
+
text = raw.decode("utf-8", errors="replace").strip()
|
|
48
|
+
if len(text) > max_chars:
|
|
49
|
+
text = text[:max_chars] + "...(truncated)"
|
|
50
|
+
return text
|
|
51
|
+
except Exception as ex:
|
|
52
|
+
return f"(decode error: {ex})"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# --------------------------------------------------------------------------
|
|
56
|
+
# 业务成功判定
|
|
57
|
+
# --------------------------------------------------------------------------
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def is_api_success(response_data: Dict[str, Any]) -> bool:
|
|
61
|
+
"""判断 API 业务是否成功.
|
|
62
|
+
|
|
63
|
+
与旧版的差异:
|
|
64
|
+
- 不再要求"必须有 data 才算成功". delete/cancel 等接口正常返回 None 不会被误判
|
|
65
|
+
- 仅依据 errors / exc / http_status_code (frappe 内嵌) 判定
|
|
66
|
+
"""
|
|
67
|
+
if not isinstance(response_data, dict):
|
|
68
|
+
return False
|
|
69
|
+
if response_data.get("errors"):
|
|
70
|
+
return False
|
|
71
|
+
if response_data.get("exc"):
|
|
72
|
+
return False
|
|
73
|
+
inner_status = response_data.get("http_status_code")
|
|
74
|
+
if inner_status is not None:
|
|
75
|
+
try:
|
|
76
|
+
return 200 <= int(inner_status) < 300
|
|
77
|
+
except (TypeError, ValueError):
|
|
78
|
+
return False
|
|
79
|
+
return True
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def extract_success_data(response_data: Dict[str, Any]) -> Any:
|
|
83
|
+
"""从成功响应中提取数据. 优先 data 字段, 其次 messages 字段."""
|
|
84
|
+
data = response_data.get("data")
|
|
85
|
+
if data is not None:
|
|
86
|
+
return data
|
|
87
|
+
messages = response_data.get("messages")
|
|
88
|
+
if messages is not None:
|
|
89
|
+
return messages
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# --------------------------------------------------------------------------
|
|
94
|
+
# messages 红色 / warning 启发式分流
|
|
95
|
+
# --------------------------------------------------------------------------
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
_RED_INDICATOR = "red"
|
|
99
|
+
_ERROR_TITLES = {"错误", "Error", "ERROR"}
|
|
100
|
+
# frappe _get_missing_mandatory_fields() 等默认友好文案的前缀, 用于启发式判定:
|
|
101
|
+
# "Error: Value missing for ..." / "错误: ..." / "错误:..." (中文全角冒号)
|
|
102
|
+
_ERROR_TEXT_PREFIXES = ("错误:", "错误:", "Error:", "ERROR:", "error:")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _is_error_message(msg: Dict[str, Any]) -> bool:
|
|
106
|
+
"""判定一条 message 是否是错误补充文案 (vs 普通提示).
|
|
107
|
+
|
|
108
|
+
frappe 在调用 ``frappe.throw`` / ``MandatoryError`` 时, 默认 msgprint 不设
|
|
109
|
+
indicator 也不设 title, 但 message 文本本身会以 ``错误:`` / ``Error:`` 开头.
|
|
110
|
+
所以判定要走多重启发式:
|
|
111
|
+
1. ``indicator == "red"``
|
|
112
|
+
2. ``title in {"错误", "Error", "ERROR"}``
|
|
113
|
+
3. ``raise_exception`` 标记
|
|
114
|
+
4. message 文本以错误前缀开头
|
|
115
|
+
"""
|
|
116
|
+
if (msg.get("indicator") or "").strip().lower() == _RED_INDICATOR:
|
|
117
|
+
return True
|
|
118
|
+
if (msg.get("title") or "").strip() in _ERROR_TITLES:
|
|
119
|
+
return True
|
|
120
|
+
if msg.get("raise_exception"):
|
|
121
|
+
return True
|
|
122
|
+
text = (msg.get("message") or "").strip()
|
|
123
|
+
return text.startswith(_ERROR_TEXT_PREFIXES)
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def split_messages(raw_messages: Any) -> tuple[List[str], List[str]]:
|
|
127
|
+
"""把 frappe messages[] 拆成 (error_texts, warning_texts).
|
|
128
|
+
|
|
129
|
+
判定基于 ``_is_error_message`` 多重启发式 (indicator=red / title=错误 / message
|
|
130
|
+
文本以 "错误:" / "Error:" 开头 / raise_exception 标记). 命中即归错误文案,
|
|
131
|
+
否则进 warnings.
|
|
132
|
+
"""
|
|
133
|
+
error_texts: List[str] = []
|
|
134
|
+
warnings: List[str] = []
|
|
135
|
+
if not isinstance(raw_messages, list):
|
|
136
|
+
return error_texts, warnings
|
|
137
|
+
|
|
138
|
+
for msg in raw_messages:
|
|
139
|
+
if not isinstance(msg, dict):
|
|
140
|
+
text = str(msg).strip()
|
|
141
|
+
if text:
|
|
142
|
+
warnings.append(text)
|
|
143
|
+
continue
|
|
144
|
+
text = (msg.get("message") or "").strip()
|
|
145
|
+
if not text:
|
|
146
|
+
continue
|
|
147
|
+
if _is_error_message(msg):
|
|
148
|
+
error_texts.append(text)
|
|
149
|
+
else:
|
|
150
|
+
warnings.append(text)
|
|
151
|
+
|
|
152
|
+
return error_texts, warnings
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def merge_error_message(primary: str, extras: List[str]) -> str:
|
|
156
|
+
"""把 frappe messages 里的友好文案合并到主错误 message, 去重去空."""
|
|
157
|
+
pieces: List[str] = []
|
|
158
|
+
if primary:
|
|
159
|
+
pieces.append(primary)
|
|
160
|
+
for extra in extras:
|
|
161
|
+
if extra and extra not in pieces:
|
|
162
|
+
pieces.append(extra)
|
|
163
|
+
return "; ".join(pieces)
|
|
@@ -10,7 +10,8 @@ from logging import getLogger
|
|
|
10
10
|
from .type import UserProfile
|
|
11
11
|
from .profile import UserProfileInfo
|
|
12
12
|
from .auth import AuthHeader
|
|
13
|
-
from .resp import
|
|
13
|
+
from .resp import ApiError, ApiResponse, _build_network_error_response, parse_response
|
|
14
|
+
from .error_code import FiuaiErrorCode
|
|
14
15
|
from .context import get_current_headers
|
|
15
16
|
|
|
16
17
|
|
|
@@ -220,11 +221,12 @@ class FiuaiSDK(object):
|
|
|
220
221
|
|
|
221
222
|
def get_avaliable_company(self, page: int=1, page_size: int=20, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
222
223
|
headers = self._get_merged_headers(extra_headers)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
224
|
+
return self._safe_request(
|
|
225
|
+
"GET",
|
|
226
|
+
self.url + "/api/method/fiuai.network.doctype.company.company.get_available_companies",
|
|
227
|
+
params={"page": page, "page_size": page_size},
|
|
228
|
+
headers=headers,
|
|
229
|
+
)
|
|
228
230
|
|
|
229
231
|
def swith_company(self, tenant: str = "", company: str = "") -> ApiResponse:
|
|
230
232
|
|
|
@@ -266,37 +268,60 @@ class FiuaiSDK(object):
|
|
|
266
268
|
user_id = self.headers.x_fiuai_user
|
|
267
269
|
|
|
268
270
|
headers = self._get_merged_headers(extra_headers)
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
271
|
+
profile_response = self._safe_request(
|
|
272
|
+
"GET",
|
|
273
|
+
self.url + f"/api/v2/internal/user/profile/{user_id}",
|
|
274
|
+
headers=headers,
|
|
275
|
+
)
|
|
272
276
|
|
|
273
|
-
if profile_response.is_success():
|
|
274
|
-
try:
|
|
275
|
-
profile_response.data = UserProfileInfo.model_validate(profile_response.data)
|
|
276
|
-
except Exception as e:
|
|
277
|
-
profile_response.error = str(e)
|
|
278
|
-
profile_response.error_code = "PROFILE_FORMAT_ERROR"
|
|
279
|
-
return profile_response
|
|
280
|
-
return profile_response
|
|
281
|
-
else:
|
|
277
|
+
if not profile_response.is_success():
|
|
282
278
|
return profile_response
|
|
283
279
|
|
|
280
|
+
try:
|
|
281
|
+
profile_response.data = UserProfileInfo.model_validate(profile_response.data)
|
|
282
|
+
except Exception as e:
|
|
283
|
+
logger.warning("UserProfileInfo parse failed for user=%s: %s", user_id, e)
|
|
284
|
+
err = ApiError(
|
|
285
|
+
code=FiuaiErrorCode.INTERNAL_VALIDATION_ERROR.value,
|
|
286
|
+
type="ProfileFormatError",
|
|
287
|
+
message="用户信息格式异常,请联系管理员",
|
|
288
|
+
detail=str(e),
|
|
289
|
+
http_status_code=profile_response.status_code,
|
|
290
|
+
)
|
|
291
|
+
profile_response.api_success = False
|
|
292
|
+
profile_response.error = err
|
|
293
|
+
profile_response.errors = [err]
|
|
294
|
+
profile_response.data = None
|
|
295
|
+
return profile_response
|
|
296
|
+
|
|
284
297
|
|
|
285
298
|
|
|
286
299
|
def internal_post_req(self, uri, postdata={}, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
287
300
|
headers = self._get_merged_headers(extra_headers)
|
|
288
|
-
|
|
289
|
-
|
|
301
|
+
return self._safe_request(
|
|
302
|
+
"POST",
|
|
303
|
+
self.url + '/api/v2/internal/' + uri.lstrip('/'),
|
|
304
|
+
data=postdata,
|
|
305
|
+
headers=headers,
|
|
306
|
+
)
|
|
290
307
|
|
|
291
308
|
def internal_get_req(self, uri, params={}, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
292
309
|
headers = self._get_merged_headers(extra_headers)
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
310
|
+
return self._safe_request(
|
|
311
|
+
"GET",
|
|
312
|
+
self.url + '/api/v2/internal/' + uri.lstrip('/'),
|
|
313
|
+
params=params,
|
|
314
|
+
headers=headers,
|
|
315
|
+
)
|
|
316
|
+
|
|
296
317
|
def internal_create(self, data={}, auto_submit: bool = False, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
297
318
|
headers = self._get_merged_headers(extra_headers)
|
|
298
|
-
|
|
299
|
-
|
|
319
|
+
return self._safe_request(
|
|
320
|
+
"POST",
|
|
321
|
+
self.url + '/api/v2/internal/doctype/create',
|
|
322
|
+
data={"data": json.dumps(data, ensure_ascii=False), "auto_submit": auto_submit},
|
|
323
|
+
headers=headers,
|
|
324
|
+
)
|
|
300
325
|
|
|
301
326
|
def internal_get(self, doctype, name, fields=None, filters=None, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
302
327
|
d = {
|
|
@@ -308,8 +333,12 @@ class FiuaiSDK(object):
|
|
|
308
333
|
if filters:
|
|
309
334
|
d["filters"] = json.dumps(filters)
|
|
310
335
|
headers = self._get_merged_headers(extra_headers)
|
|
311
|
-
|
|
312
|
-
|
|
336
|
+
return self._safe_request(
|
|
337
|
+
"GET",
|
|
338
|
+
self.url + '/api/v2/internal/doctype/get',
|
|
339
|
+
params=d,
|
|
340
|
+
headers=headers,
|
|
341
|
+
)
|
|
313
342
|
|
|
314
343
|
def get_decrypted_password(
|
|
315
344
|
self,
|
|
@@ -321,12 +350,12 @@ class FiuaiSDK(object):
|
|
|
321
350
|
"""从 Frappe __Auth 读取 Password 类型字段明文 (internal get_password)."""
|
|
322
351
|
params = {"doctype": doctype, "name": name, "fieldname": fieldname}
|
|
323
352
|
headers = self._get_merged_headers(extra_headers)
|
|
324
|
-
|
|
353
|
+
resp = self._safe_request(
|
|
354
|
+
"GET",
|
|
325
355
|
self.url + "/api/v2/internal/doctype/get_password",
|
|
326
356
|
params=params,
|
|
327
357
|
headers=headers,
|
|
328
358
|
)
|
|
329
|
-
resp = self.post_process(res)
|
|
330
359
|
if resp.is_success() and resp.data and isinstance(resp.data, dict):
|
|
331
360
|
pw = resp.data.get("password")
|
|
332
361
|
return pw if isinstance(pw, str) else None
|
|
@@ -346,76 +375,169 @@ class FiuaiSDK(object):
|
|
|
346
375
|
if order_by:
|
|
347
376
|
d["order_by"] = order_by
|
|
348
377
|
headers = self._get_merged_headers(extra_headers)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
378
|
+
return self._safe_request(
|
|
379
|
+
"GET",
|
|
380
|
+
self.url + '/api/v2/internal/doctype/get_list',
|
|
381
|
+
params=d,
|
|
382
|
+
headers=headers,
|
|
383
|
+
)
|
|
354
384
|
|
|
355
|
-
|
|
356
385
|
def internal_update(self, data={}, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
357
386
|
headers = self._get_merged_headers(extra_headers)
|
|
358
|
-
|
|
359
|
-
|
|
387
|
+
return self._safe_request(
|
|
388
|
+
"POST",
|
|
389
|
+
self.url + '/api/v2/internal/doctype/update',
|
|
390
|
+
data={"data": json.dumps(data, ensure_ascii=False)},
|
|
391
|
+
headers=headers,
|
|
392
|
+
)
|
|
360
393
|
|
|
361
394
|
def internal_delete(self, doctype, name, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
362
395
|
headers = self._get_merged_headers(extra_headers)
|
|
363
|
-
|
|
364
|
-
|
|
396
|
+
return self._safe_request(
|
|
397
|
+
"POST",
|
|
398
|
+
self.url + '/api/v2/internal/doctype/delete',
|
|
399
|
+
data={"doctype": doctype, "name": name},
|
|
400
|
+
headers=headers,
|
|
401
|
+
)
|
|
365
402
|
|
|
366
403
|
def internal_submit(self, doctype, name, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
367
404
|
headers = self._get_merged_headers(extra_headers)
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
405
|
+
return self._safe_request(
|
|
406
|
+
"POST",
|
|
407
|
+
self.url + '/api/v2/internal/doctype/submit',
|
|
408
|
+
data={"doctype": doctype, "name": name},
|
|
409
|
+
headers=headers,
|
|
410
|
+
)
|
|
411
|
+
|
|
371
412
|
def internal_cancel(self, doctype, name, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
372
413
|
headers = self._get_merged_headers(extra_headers)
|
|
373
|
-
|
|
374
|
-
|
|
414
|
+
return self._safe_request(
|
|
415
|
+
"POST",
|
|
416
|
+
self.url + '/api/v2/internal/doctype/cancel',
|
|
417
|
+
data={"doctype": doctype, "name": name},
|
|
418
|
+
headers=headers,
|
|
419
|
+
)
|
|
375
420
|
|
|
376
421
|
|
|
377
422
|
######## 特殊接口 ########
|
|
378
423
|
def get_meta(self, doctype: str, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
379
424
|
headers = self._get_merged_headers(extra_headers)
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
425
|
+
return self._safe_request(
|
|
426
|
+
"GET",
|
|
427
|
+
self.url + '/api/v2/internal/doctype/meta/' + doctype,
|
|
428
|
+
headers=headers,
|
|
429
|
+
)
|
|
384
430
|
|
|
385
431
|
def get_ontology_event(self, event_id: str, extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
386
432
|
headers = self._get_merged_headers(extra_headers)
|
|
387
|
-
|
|
388
|
-
|
|
433
|
+
return self._safe_request(
|
|
434
|
+
"GET",
|
|
435
|
+
self.url + '/api/v2/internal/ontology/event/' + event_id,
|
|
436
|
+
headers=headers,
|
|
437
|
+
)
|
|
389
438
|
|
|
390
439
|
def get_ontology_event_list(self, event_filter: Dict[str, Any], extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
391
|
-
|
|
392
440
|
headers = self._get_merged_headers(extra_headers)
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
return self.post_process(res)
|
|
441
|
+
return self._safe_request(
|
|
442
|
+
"GET",
|
|
443
|
+
self.url + '/api/v2/internal/ontology/event',
|
|
444
|
+
params={"event_filter": quote(json.dumps(event_filter, ensure_ascii=False))},
|
|
445
|
+
headers=headers,
|
|
446
|
+
)
|
|
400
447
|
|
|
401
448
|
def add_ontology_event(self, data: Dict[str, Any], extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
402
449
|
headers = self._get_merged_headers(extra_headers)
|
|
403
|
-
|
|
404
|
-
|
|
450
|
+
return self._safe_request(
|
|
451
|
+
"POST",
|
|
452
|
+
self.url + '/api/v2/internal/ontology/event/add',
|
|
453
|
+
data={"data": json.dumps(data, ensure_ascii=False)},
|
|
454
|
+
headers=headers,
|
|
455
|
+
)
|
|
405
456
|
|
|
406
457
|
def update_ontology_event(self, data: Dict[str, Any], extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
407
|
-
|
|
408
458
|
headers = self._get_merged_headers(extra_headers)
|
|
409
|
-
|
|
410
|
-
|
|
459
|
+
return self._safe_request(
|
|
460
|
+
"POST",
|
|
461
|
+
self.url + '/api/v2/internal/ontology/event',
|
|
462
|
+
data={"data": json.dumps(data, ensure_ascii=False)},
|
|
463
|
+
headers=headers,
|
|
464
|
+
)
|
|
411
465
|
|
|
412
466
|
def batch_add_ontology_event(self, data: List[Dict[str, Any]], extra_headers: Optional[Dict[str, Any]] = None) -> ApiResponse:
|
|
413
467
|
headers = self._get_merged_headers(extra_headers)
|
|
414
|
-
|
|
415
|
-
|
|
468
|
+
return self._safe_request(
|
|
469
|
+
"POST",
|
|
470
|
+
self.url + '/api/v2/internal/ontology/event/batch_add',
|
|
471
|
+
data={"data": json.dumps(data, ensure_ascii=False)},
|
|
472
|
+
headers=headers,
|
|
473
|
+
)
|
|
416
474
|
|
|
417
475
|
|
|
418
476
|
######## 数据处理 ########
|
|
477
|
+
def _safe_request(self, method: str, url: str, **kwargs) -> ApiResponse:
|
|
478
|
+
"""统一的 HTTP 请求封装, 兜底捕获 httpx 网络异常.
|
|
479
|
+
|
|
480
|
+
SDK 对外契约: 永远返回 ``ApiResponse``, 不抛 httpx 异常. 调用方只需读
|
|
481
|
+
``resp.is_success()`` / ``resp.error.code`` 即可分流业务/网络错误.
|
|
482
|
+
|
|
483
|
+
Args:
|
|
484
|
+
method: HTTP 方法, "GET" / "POST" (大小写不敏感)
|
|
485
|
+
url: 完整请求 URL
|
|
486
|
+
**kwargs: 透传给 ``httpx.Client.{get,post}`` 的参数
|
|
487
|
+
|
|
488
|
+
Returns:
|
|
489
|
+
ApiResponse: 结构化响应. 网络异常时 ``error.code`` 为
|
|
490
|
+
``NETWORK_ERROR`` / ``SERVER_ERROR``.
|
|
491
|
+
"""
|
|
492
|
+
method_upper = method.upper()
|
|
493
|
+
try:
|
|
494
|
+
if method_upper == "GET":
|
|
495
|
+
res = self.client.get(url, **kwargs)
|
|
496
|
+
elif method_upper == "POST":
|
|
497
|
+
res = self.client.post(url, **kwargs)
|
|
498
|
+
else:
|
|
499
|
+
logger.error("Unsupported HTTP method: %s", method)
|
|
500
|
+
return _build_network_error_response(
|
|
501
|
+
code=FiuaiErrorCode.SERVER_ERROR,
|
|
502
|
+
message=f"Unsupported HTTP method: {method}",
|
|
503
|
+
status_code=500,
|
|
504
|
+
http_success=False,
|
|
505
|
+
)
|
|
506
|
+
except httpx.TimeoutException as e:
|
|
507
|
+
logger.warning("HTTP timeout: %s %s, detail=%s", method_upper, url, e)
|
|
508
|
+
return _build_network_error_response(
|
|
509
|
+
code=FiuaiErrorCode.NETWORK_ERROR,
|
|
510
|
+
message="请求超时,请稍后重试",
|
|
511
|
+
status_code=504,
|
|
512
|
+
http_success=False,
|
|
513
|
+
detail=str(e),
|
|
514
|
+
)
|
|
515
|
+
except httpx.RequestError as e:
|
|
516
|
+
logger.warning(
|
|
517
|
+
"HTTP request error: %s %s, type=%s, detail=%s",
|
|
518
|
+
method_upper,
|
|
519
|
+
url,
|
|
520
|
+
type(e).__name__,
|
|
521
|
+
e,
|
|
522
|
+
)
|
|
523
|
+
return _build_network_error_response(
|
|
524
|
+
code=FiuaiErrorCode.NETWORK_ERROR,
|
|
525
|
+
message=f"网络异常: {type(e).__name__}",
|
|
526
|
+
status_code=502,
|
|
527
|
+
http_success=False,
|
|
528
|
+
detail=str(e),
|
|
529
|
+
)
|
|
530
|
+
except Exception as e:
|
|
531
|
+
logger.exception("Unexpected HTTP error: %s %s", method_upper, url)
|
|
532
|
+
return _build_network_error_response(
|
|
533
|
+
code=FiuaiErrorCode.SERVER_ERROR,
|
|
534
|
+
message="请求执行异常",
|
|
535
|
+
status_code=500,
|
|
536
|
+
http_success=False,
|
|
537
|
+
detail=str(e),
|
|
538
|
+
)
|
|
539
|
+
return self.post_process(res)
|
|
540
|
+
|
|
419
541
|
def post_process(self, response) -> ApiResponse:
|
|
420
542
|
"""
|
|
421
543
|
处理API响应,使用结构化的错误处理系统
|