fiuai-sdk-python 0.12.0__tar.gz → 0.14.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.12.0 → fiuai_sdk_python-0.14.0}/CHANGELOG.md +13 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/PKG-INFO +1 -1
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/pyproject.toml +1 -1
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/__init__.py +1 -1
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/client.py +187 -65
- fiuai_sdk_python-0.14.0/src/fiuai_sdk_python/const.py +46 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/resp.py +90 -7
- fiuai_sdk_python-0.12.0/src/fiuai_sdk_python/const.py +0 -18
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/.gitignore +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/LICENSE +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/README.md +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/_resp_helpers.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/auth/__init__.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/auth/context_mgr.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/auth/header.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/auth/helper.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/auth/type.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/bank.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/company.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/config.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/context.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/datatype.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/doctype.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/error.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/error_code.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/error_parsers.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/examples/fastapi_integration.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/http/__init__.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/http/client.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/item.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/perm.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/__init__.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/cache_client.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/circuit_breaker.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/decorator.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/redis_manager.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/types.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/scheduler/__init__.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/scheduler/runner.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/profile.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/setup.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/type.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/util.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/utils/__init__.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/utils/ids.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/utils/logger.py +0 -0
- {fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/utils/text.py +0 -0
|
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
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
|
+
|
|
8
21
|
## [Unreleased]
|
|
9
22
|
|
|
10
23
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fiuai_sdk_python
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.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,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响应,使用结构化的错误处理系统
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# -- coding: utf-8 --
|
|
2
|
+
# Project: fiuaiclient
|
|
3
|
+
# Created Date: 2025 07 We
|
|
4
|
+
# Author: liming
|
|
5
|
+
# Email: lmlala@aliyun.com
|
|
6
|
+
# Copyright (c) 2025 FiuAI
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
V2_GET_LIST_URI = "/document"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
ASSISTANT_USER = "assistant@fiuai.com"
|
|
13
|
+
|
|
14
|
+
DEFAULT_TENANT_ID = "8888888888888888888"
|
|
15
|
+
PASSIVE_TENANT_ID = "9999999999999999999"
|
|
16
|
+
UNSIGNED_TENANT_ID = "7777777777777777777"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
CORE_SYSTEM_FIELDS = [
|
|
20
|
+
"_liked_by",
|
|
21
|
+
"_assign",
|
|
22
|
+
"_comments",
|
|
23
|
+
"_user_tags",
|
|
24
|
+
"__islocal",
|
|
25
|
+
"__unsaved",
|
|
26
|
+
"__last_modified",
|
|
27
|
+
"__last_modified_by",
|
|
28
|
+
"__last_modified_by_name",
|
|
29
|
+
"creation",
|
|
30
|
+
"modified",
|
|
31
|
+
"modified_by",
|
|
32
|
+
"parent",
|
|
33
|
+
"parentfield",
|
|
34
|
+
"parenttype",
|
|
35
|
+
"owner",
|
|
36
|
+
"name",
|
|
37
|
+
"id",
|
|
38
|
+
"idx",
|
|
39
|
+
"doctype",
|
|
40
|
+
# "doc_source_from",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
ALL_SYSTEM_FIELDS = [
|
|
44
|
+
"name",
|
|
45
|
+
"docstatus"
|
|
46
|
+
] + CORE_SYSTEM_FIELDS
|
|
@@ -26,12 +26,13 @@ HTTP 响应规整化.
|
|
|
26
26
|
内部 helper 拆到 ``_resp_helpers.py`` (单文件 < 400 行规约).
|
|
27
27
|
"""
|
|
28
28
|
|
|
29
|
+
import json
|
|
29
30
|
import logging
|
|
30
31
|
import warnings
|
|
31
32
|
from typing import Any, Dict, List, Optional
|
|
32
33
|
|
|
33
34
|
from httpx import Response
|
|
34
|
-
from pydantic import BaseModel, ConfigDict, Field
|
|
35
|
+
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
|
35
36
|
|
|
36
37
|
from . import _resp_helpers as _h
|
|
37
38
|
from .error_code import (
|
|
@@ -44,6 +45,40 @@ from .error_parsers import parse_error_context
|
|
|
44
45
|
logger = logging.getLogger(__name__)
|
|
45
46
|
|
|
46
47
|
|
|
48
|
+
def _to_text(value: Any) -> str:
|
|
49
|
+
"""把任意值安全规范化成字符串.
|
|
50
|
+
|
|
51
|
+
设计目标: 上游 (frappe / 网关) 偶尔会把本该是 str 的字段返回 list/dict/None
|
|
52
|
+
(例如 ``message=['Contract already exists']``), 直接进 Pydantic 会校验失败.
|
|
53
|
+
本 helper 兜底处理:
|
|
54
|
+
- None / 空白 -> ""
|
|
55
|
+
- list -> "; ".join(各元素递归 _to_text)
|
|
56
|
+
- dict -> 优先取 message/title, 否则 json.dumps
|
|
57
|
+
- 其他 -> str(value)
|
|
58
|
+
"""
|
|
59
|
+
if value is None:
|
|
60
|
+
return ""
|
|
61
|
+
if isinstance(value, str):
|
|
62
|
+
return value
|
|
63
|
+
if isinstance(value, list):
|
|
64
|
+
parts: List[str] = []
|
|
65
|
+
for item in value:
|
|
66
|
+
text = _to_text(item)
|
|
67
|
+
if text and text.strip():
|
|
68
|
+
parts.append(text)
|
|
69
|
+
return "; ".join(parts)
|
|
70
|
+
if isinstance(value, dict):
|
|
71
|
+
for key in ("message", "title", "detail"):
|
|
72
|
+
candidate = value.get(key)
|
|
73
|
+
if candidate:
|
|
74
|
+
return _to_text(candidate)
|
|
75
|
+
try:
|
|
76
|
+
return json.dumps(value, ensure_ascii=False)
|
|
77
|
+
except Exception:
|
|
78
|
+
return str(value)
|
|
79
|
+
return str(value)
|
|
80
|
+
|
|
81
|
+
|
|
47
82
|
# 历史调试样例, 保留作为协议参考 (frappe v2 errors + messages 双源结构):
|
|
48
83
|
# {
|
|
49
84
|
# 'errors': [{
|
|
@@ -84,6 +119,36 @@ class ApiError(BaseModel):
|
|
|
84
119
|
http_status_code: int = Field(default=500, description="HTTP 状态码")
|
|
85
120
|
traceback: Optional[str] = Field(default=None, description="异常 traceback (仅 dev)")
|
|
86
121
|
|
|
122
|
+
@field_validator("code", "type", "message", mode="before")
|
|
123
|
+
@classmethod
|
|
124
|
+
def _coerce_required_text(cls, value: Any, info: Any) -> str:
|
|
125
|
+
"""str 必填字段的兜底规范化: list/dict/None 也不会触发 Pydantic 校验失败.
|
|
126
|
+
|
|
127
|
+
- code 为 None/空 时回退到 ``FiuaiErrorCode.UNKNOWN.value``, 保住错误码语义
|
|
128
|
+
- message/type 为 None 时降级为空字符串
|
|
129
|
+
"""
|
|
130
|
+
if value is None or (isinstance(value, str) and not value):
|
|
131
|
+
if info.field_name == "code":
|
|
132
|
+
return FiuaiErrorCode.UNKNOWN.value
|
|
133
|
+
return ""
|
|
134
|
+
if isinstance(value, str):
|
|
135
|
+
return value
|
|
136
|
+
text = _to_text(value)
|
|
137
|
+
if not text and info.field_name == "code":
|
|
138
|
+
return FiuaiErrorCode.UNKNOWN.value
|
|
139
|
+
return text
|
|
140
|
+
|
|
141
|
+
@field_validator("detail", "traceback", mode="before")
|
|
142
|
+
@classmethod
|
|
143
|
+
def _coerce_optional_text(cls, value: Any) -> Optional[str]:
|
|
144
|
+
"""Optional[str] 字段保留 None 语义, 但 list/dict 仍兜底成 str."""
|
|
145
|
+
if value is None:
|
|
146
|
+
return None
|
|
147
|
+
if isinstance(value, str):
|
|
148
|
+
return value
|
|
149
|
+
text = _to_text(value)
|
|
150
|
+
return text or None
|
|
151
|
+
|
|
87
152
|
|
|
88
153
|
class ApiResponse(BaseModel):
|
|
89
154
|
"""API 响应结构体.
|
|
@@ -172,18 +237,20 @@ def _normalize_error_dict(raw_error: Dict[str, Any], http_status_code: int) -> A
|
|
|
172
237
|
if not isinstance(raw_error, dict):
|
|
173
238
|
raw_error = {}
|
|
174
239
|
|
|
175
|
-
exception_type = (raw_error.get("type")
|
|
176
|
-
raw_message = raw_error.get("message")
|
|
177
|
-
display_message = raw_error.get("display_message")
|
|
178
|
-
title = raw_error.get("title")
|
|
240
|
+
exception_type = _to_text(raw_error.get("type")).strip()
|
|
241
|
+
raw_message = _to_text(raw_error.get("message"))
|
|
242
|
+
display_message = _to_text(raw_error.get("display_message"))
|
|
243
|
+
title = _to_text(raw_error.get("title"))
|
|
179
244
|
|
|
180
|
-
|
|
245
|
+
code_value = raw_error.get("code")
|
|
246
|
+
code_str = _to_text(code_value).strip() if code_value is not None else ""
|
|
181
247
|
if not code_str:
|
|
182
248
|
code_str = exception_to_code(exception_type).value
|
|
183
249
|
if code_str == FiuaiErrorCode.UNKNOWN.value:
|
|
184
250
|
code_str = status_code_to_code(http_status_code).value
|
|
185
251
|
|
|
186
|
-
|
|
252
|
+
raw_context = raw_error.get("context")
|
|
253
|
+
context: Dict[str, Any] = raw_context if isinstance(raw_context, dict) else {}
|
|
187
254
|
if not context and exception_type and raw_message:
|
|
188
255
|
context = parse_error_context(exception_type, raw_message)
|
|
189
256
|
|
|
@@ -336,6 +403,22 @@ def parse_response(response: Optional[Response]) -> ApiResponse:
|
|
|
336
403
|
Returns:
|
|
337
404
|
ApiResponse: 结构化的 API 响应, 一定不会抛异常
|
|
338
405
|
"""
|
|
406
|
+
try:
|
|
407
|
+
return _parse_response_impl(response)
|
|
408
|
+
except Exception as e:
|
|
409
|
+
logger.exception("parse_response unexpected failure")
|
|
410
|
+
fallback_status = getattr(response, "status_code", 0) or 500
|
|
411
|
+
return _build_network_error_response(
|
|
412
|
+
code=FiuaiErrorCode.SERVER_ERROR,
|
|
413
|
+
message="SDK 解析响应时发生未预期错误,请联系管理员",
|
|
414
|
+
status_code=fallback_status,
|
|
415
|
+
http_success=False,
|
|
416
|
+
detail=str(e),
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def _parse_response_impl(response: Optional[Response]) -> ApiResponse:
|
|
421
|
+
"""``parse_response`` 主体实现, 任何异常由外层 ``parse_response`` 统一兜底."""
|
|
339
422
|
if response is None:
|
|
340
423
|
return _build_network_error_response(
|
|
341
424
|
code=FiuaiErrorCode.API_NO_RESPONSE,
|
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
# -- coding: utf-8 --
|
|
2
|
-
# Project: fiuaiclient
|
|
3
|
-
# Created Date: 2025 07 We
|
|
4
|
-
# Author: liming
|
|
5
|
-
# Email: lmlala@aliyun.com
|
|
6
|
-
# Copyright (c) 2025 FiuAI
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
V2_GET_LIST_URI = "/document"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
ASSISTANT_USER = "assistant@fiuai.com"
|
|
13
|
-
|
|
14
|
-
DEFAULT_TENANT_ID = "8888888888888888888"
|
|
15
|
-
PASSIVE_TENANT_ID = "9999999999999999999"
|
|
16
|
-
UNSIGNED_TENANT_ID = "7777777777777777777"
|
|
17
|
-
|
|
18
|
-
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/auth/context_mgr.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/__init__.py
RENAMED
|
File without changes
|
{fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/cache_client.py
RENAMED
|
File without changes
|
|
File without changes
|
{fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/decorator.py
RENAMED
|
File without changes
|
{fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/cache/redis_manager.py
RENAMED
|
File without changes
|
|
File without changes
|
{fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/scheduler/__init__.py
RENAMED
|
File without changes
|
{fiuai_sdk_python-0.12.0 → fiuai_sdk_python-0.14.0}/src/fiuai_sdk_python/pkg/scheduler/runner.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|