sdpy-kit 0.6.9__tar.gz → 0.6.11__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 (60) hide show
  1. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/PKG-INFO +20 -10
  2. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/README.md +13 -4
  3. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/__init__.py +1 -1
  4. sdpy_kit-0.6.11/kit/ai/_retry_after.py +51 -0
  5. sdpy_kit-0.6.11/kit/ai/_service_base.py +115 -0
  6. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/chat_service.py +164 -199
  7. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/embedding_service.py +30 -40
  8. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/image_service.py +28 -38
  9. sdpy_kit-0.6.11/kit/ai/model_client.py +59 -0
  10. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/rerank_service.py +29 -6
  11. sdpy_kit-0.6.11/kit/ai/rerank_transport.py +138 -0
  12. sdpy_kit-0.6.11/kit/ai/sdk_client.py +379 -0
  13. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/pyproject.toml +6 -5
  14. sdpy_kit-0.6.9/kit/ai/base_service.py +0 -215
  15. sdpy_kit-0.6.9/kit/ai/model_client.py +0 -278
  16. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/.gitignore +0 -0
  17. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/__init__.py +0 -0
  18. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/exceptions.py +0 -0
  19. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/failover.py +0 -0
  20. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/model_builder.py +0 -0
  21. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/pool.py +0 -0
  22. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/profiles.py +0 -0
  23. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/runner.py +0 -0
  24. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/ai/types.py +0 -0
  25. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/common_kit.py +0 -0
  26. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/config/__init__.py +0 -0
  27. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/config/kit_config.py +0 -0
  28. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/config/log_config.py +0 -0
  29. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/crawl/__init__.py +0 -0
  30. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/crawl/article_io.py +0 -0
  31. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/crawl/article_models.py +0 -0
  32. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/crawl/deepseek_kit.py +0 -0
  33. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/crawl/web_article_kit.py +0 -0
  34. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/crawl/wx_article_kit.py +0 -0
  35. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/__init__.py +0 -0
  36. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/audit.py +0 -0
  37. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/base_db_kit.py +0 -0
  38. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/crud.py +0 -0
  39. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/engine.py +0 -0
  40. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/exceptions.py +0 -0
  41. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/models/__init__.py +0 -0
  42. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/models/base.py +0 -0
  43. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/mysql_kit.py +0 -0
  44. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/pg_kit.py +0 -0
  45. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/query.py +0 -0
  46. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/session.py +0 -0
  47. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/sqlite_kit.py +0 -0
  48. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/db/transaction.py +0 -0
  49. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/decima_kit.py +0 -0
  50. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/docs/__init__.py +0 -0
  51. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/docs/anydoc_kit.py +0 -0
  52. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/docs/docx_kit.py +0 -0
  53. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/docs/marker_kit.py +0 -0
  54. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/docs/md2doc_kit.py +0 -0
  55. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/docs/mineru_kit.py +0 -0
  56. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/docs/rc_docparser_kit.py +0 -0
  57. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/file_kit.py +0 -0
  58. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/laya_kit.py +0 -0
  59. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/license_kit.py +0 -0
  60. {sdpy_kit-0.6.9 → sdpy_kit-0.6.11}/kit/redis_kit.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: sdpy-kit
3
- Version: 0.6.9
3
+ Version: 0.6.11
4
4
  Summary: SDPY Kit——统一数据库访问层(PG/MySQL/SQLite)与 AI/Redis/文件/文档/向量工具集
5
5
  Author-email: Clark <changhongyuan@126.com>
6
6
  License-Expression: MIT
@@ -24,6 +24,7 @@ Requires-Dist: sqlalchemy~=2.0.54
24
24
  Requires-Dist: sqlmodel>=0.0.48
25
25
  Provides-Extra: ai
26
26
  Requires-Dist: httpx>=0.28.1; extra == 'ai'
27
+ Requires-Dist: openai<4,>=3.44; extra == 'ai'
27
28
  Requires-Dist: python-dotenv>=1.2.4; extra == 'ai'
28
29
  Provides-Extra: all
29
30
  Requires-Dist: decima-ai>=2.5.1; extra == 'all'
@@ -31,11 +32,11 @@ Requires-Dist: docxtpl>=0.20.2; extra == 'all'
31
32
  Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'all'
32
33
  Requires-Dist: httpx>=0.28.1; extra == 'all'
33
34
  Requires-Dist: huggingface-hub<2,>=1.5; extra == 'all'
34
- Requires-Dist: laya>=0.4.21; extra == 'all'
35
+ Requires-Dist: laya>=0.4.22; extra == 'all'
35
36
  Requires-Dist: markdown-it-py>=4.2.0; extra == 'all'
36
- Requires-Dist: marker-pdf>=2.0.0; extra == 'all'
37
37
  Requires-Dist: numpy>=2.5.4; extra == 'all'
38
38
  Requires-Dist: onnxruntime>=1.18; extra == 'all'
39
+ Requires-Dist: openai<4,>=3.44; extra == 'all'
39
40
  Requires-Dist: pikepdf>=10.17.0; extra == 'all'
40
41
  Requires-Dist: pimd[pdfa]>=2.2.5; extra == 'all'
41
42
  Requires-Dist: psycopg2-binary>=2.9.13; extra == 'all'
@@ -44,7 +45,7 @@ Requires-Dist: python-dotenv>=1.2.4; extra == 'all'
44
45
  Requires-Dist: rc-docparser>=0.2.2; extra == 'all'
45
46
  Requires-Dist: redis>=8.1.0; extra == 'all'
46
47
  Requires-Dist: requests>=2.34.2; extra == 'all'
47
- Requires-Dist: sqliteai-vector>=1.1.2; extra == 'all'
48
+ Requires-Dist: sqliteai-vector>=1.2.0; extra == 'all'
48
49
  Requires-Dist: trafilatura>=2.3.1; extra == 'all'
49
50
  Provides-Extra: anydoc
50
51
  Requires-Dist: firecrawl-anydoc>=0.2.4; extra == 'anydoc'
@@ -59,7 +60,7 @@ Provides-Extra: docx-parse
59
60
  Requires-Dist: rc-docparser>=0.2.2; extra == 'docx-parse'
60
61
  Provides-Extra: laya
61
62
  Requires-Dist: huggingface-hub<2,>=1.5; extra == 'laya'
62
- Requires-Dist: laya>=0.4.21; extra == 'laya'
63
+ Requires-Dist: laya>=0.4.22; extra == 'laya'
63
64
  Provides-Extra: marker
64
65
  Requires-Dist: marker-pdf>=2.0.0; extra == 'marker'
65
66
  Provides-Extra: md2doc
@@ -75,7 +76,7 @@ Provides-Extra: redis
75
76
  Requires-Dist: redis>=8.1.0; extra == 'redis'
76
77
  Provides-Extra: sqliteai
77
78
  Requires-Dist: numpy>=2.5.4; extra == 'sqliteai'
78
- Requires-Dist: sqliteai-vector>=1.1.2; extra == 'sqliteai'
79
+ Requires-Dist: sqliteai-vector>=1.2.0; extra == 'sqliteai'
79
80
  Provides-Extra: web-article
80
81
  Requires-Dist: trafilatura>=2.3.1; extra == 'web-article'
81
82
  Description-Content-Type: text/markdown
@@ -131,17 +132,17 @@ uv add sdpy-kit
131
132
  | `mysql` | MySqlKit(MySQL 驱动;向量仅存储,需 MySQL 9.x) | `pip install "sdpy-kit[mysql]"` |
132
133
  | `sqliteai` | SQLiteKit 向量方法(numpy / sqliteai-vector,SQLite 向量扩展) | `pip install "sdpy-kit[sqliteai]"` |
133
134
  | `redis` | RedisKit | `pip install "sdpy-kit[redis]"` |
134
- | `ai` | AI 服务池(httpx + python-dotenv) | `pip install "sdpy-kit[ai]"` |
135
+ | `ai` | AI 服务池(openai>=3.44 + httpx + python-dotenv;安装体积约 +10MB,SDK 自带 httpx2) | `pip install "sdpy-kit[ai]"` |
135
136
  | `md2doc` | Md2DocKit:Markdown → Word / PDF / HTML(pimd / fpdf2,含图片) | `pip install "sdpy-kit[md2doc]"` |
136
137
  | `anydoc` | AnyDocKit 文档转 Markdown(firecrawl-anydoc + pikepdf) | `pip install "sdpy-kit[anydoc]"` |
137
138
  | `docx` | DocxKit 模板动态填充(docxtpl) | `pip install "sdpy-kit[docx]"` |
138
139
  | `docx-parse` | RcDocParserKit 离线 docx 解析(rc-docparser → Markdown + images/) | `pip install "sdpy-kit[docx-parse]"` |
139
140
  | `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
140
- | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大) | `pip install "sdpy-kit[marker]"` |
141
+ | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大)。**与 `[ai]` 互斥**:marker-pdf 锁 `openai<3`,不能与 `openai>=3.44` 同环境安装 | `pip install "sdpy-kit[marker]"` |
141
142
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
142
143
  | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
143
144
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
144
- | `all` | 全部能力 | `pip install "sdpy-kit[all]"` |
145
+ | `all` | 常用能力集合,**不含 `marker`**(0.6.11 起,避免与 `[ai]` 的 openai 约束冲突);需要 Marker 请单独装 `[marker]` | `pip install "sdpy-kit[all]"` |
145
146
 
146
147
  ## 配置(.env)
147
148
 
@@ -809,6 +810,14 @@ page = qb.paginate(page=1, per_page=20) # {"items", "total", ...}
809
810
  ## kit.ai —— AI 多模型服务池
810
811
 
811
812
  安装:`pip install "sdpy-kit[ai]"`
813
+
814
+ 传输(0.6.11):chat / embedding / image 走 `openai` SDK(`>=3.44,<4`,
815
+ 自带 `httpx2`),rerank 走 httpx 自研传输(SDK 无 `/rerank` 资源)。
816
+ 档案里的 `endpoint` **对前三类不再生效**——SDK 固定拼 `/chat/completions`、
817
+ `/embeddings`、`/images/generations`,仅 rerank 与探活仍使用该字段。
818
+
819
+ > 安装提示:`openai` 3.x 会引入 `httpx2`,与 rerank 用的 `httpx` 并存(两个包名不同,不冲突)。
820
+
812
821
  档案:项目根 `config/ai_models.json`(见文末「AI 档案文件」)
813
822
 
814
823
  ### 1. 同步门面 `AI`(推荐)
@@ -1600,7 +1609,8 @@ AI 功能需要在项目根目录提供 `config/ai_models.json`(chat / embeddi
1600
1609
 
1601
1610
  | 层级 | 字段 | 说明 |
1602
1611
  |------|------|------|
1603
- | 档案级 | `timeout` | 请求超时秒数(默认 60) |
1612
+ | 档案级 | `endpoint` | 请求路径。**0.6.10 起仅 rerank 生效**:chat / embedding / image 由 openai SDK 固定拼路径,本字段被忽略(保留仅为兼容旧配置,不报错) |
1613
+ | 档案级 | `timeout` | 请求超时秒数(默认 60);SDK 侧为单次调用总超时 |
1604
1614
  | 档案级 | `max_tokens` | 输出上限(chat 用);`AI.chat_ask/chat_complete/chat_stream` 也可按调用点传参,逐级回落:调用点 → 档案 → provider 默认 |
1605
1615
  | 档案级 | `retries` | 5xx 重试次数(默认 2;超时不重试,候选链切换才是有效重试) |
1606
1616
  | 档案级 | `extra_body` | 厂商专有请求体字段透传(如 GLM/Qwen 关闭思考的 `{"thinking": {"type": "disabled"}}`、`{"reasoning_effort": "low"}`);与服务构造的同名键冲突时**以服务的值为准**,故 `model` / `messages` / `stream` 等保留键不可覆盖;改名类需求(如 OpenAI o 系列的 `max_completion_tokens`)无法用它实现。逐调用 `thinking` 与之同键时**以逐调用为准**并打 `WARNING`。改文件后需 `reload_config(reload_file=True)` 生效 |
@@ -49,17 +49,17 @@ uv add sdpy-kit
49
49
  | `mysql` | MySqlKit(MySQL 驱动;向量仅存储,需 MySQL 9.x) | `pip install "sdpy-kit[mysql]"` |
50
50
  | `sqliteai` | SQLiteKit 向量方法(numpy / sqliteai-vector,SQLite 向量扩展) | `pip install "sdpy-kit[sqliteai]"` |
51
51
  | `redis` | RedisKit | `pip install "sdpy-kit[redis]"` |
52
- | `ai` | AI 服务池(httpx + python-dotenv) | `pip install "sdpy-kit[ai]"` |
52
+ | `ai` | AI 服务池(openai>=3.44 + httpx + python-dotenv;安装体积约 +10MB,SDK 自带 httpx2) | `pip install "sdpy-kit[ai]"` |
53
53
  | `md2doc` | Md2DocKit:Markdown → Word / PDF / HTML(pimd / fpdf2,含图片) | `pip install "sdpy-kit[md2doc]"` |
54
54
  | `anydoc` | AnyDocKit 文档转 Markdown(firecrawl-anydoc + pikepdf) | `pip install "sdpy-kit[anydoc]"` |
55
55
  | `docx` | DocxKit 模板动态填充(docxtpl) | `pip install "sdpy-kit[docx]"` |
56
56
  | `docx-parse` | RcDocParserKit 离线 docx 解析(rc-docparser → Markdown + images/) | `pip install "sdpy-kit[docx-parse]"` |
57
57
  | `mineru` | MinerUKit 解析 API(精准解析 + Agent 轻量解析) | `pip install "sdpy-kit[mineru]"` |
58
- | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大) | `pip install "sdpy-kit[marker]"` |
58
+ | `marker` | MarkerKit 本地解析(marker-pdf → Markdown + Images;PDF + 图片,依赖体积大)。**与 `[ai]` 互斥**:marker-pdf 锁 `openai<3`,不能与 `openai>=3.44` 同环境安装 | `pip install "sdpy-kit[marker]"` |
59
59
  | `laya` | LayaKit 类型化决策(laya + torch,本地小模型 choice / multi_choice / score / noul;支持多字段 / 批量 / 长文本) | `pip install "sdpy-kit[laya]"` |
60
60
  | `decima` | DecimaKit 决策模型(decima-ai + onnxruntime,**无 torch**;选项乱序不变、置信度可门控;磁盘约 137 MB) | `pip install "sdpy-kit[decima]"` |
61
61
  | `web-article` | WebArticleKit 通用网页文章抓取转存(trafilatura,纯 HTTP 无浏览器) | `pip install "sdpy-kit[web-article]"` |
62
- | `all` | 全部能力 | `pip install "sdpy-kit[all]"` |
62
+ | `all` | 常用能力集合,**不含 `marker`**(0.6.11 起,避免与 `[ai]` 的 openai 约束冲突);需要 Marker 请单独装 `[marker]` | `pip install "sdpy-kit[all]"` |
63
63
 
64
64
  ## 配置(.env)
65
65
 
@@ -727,6 +727,14 @@ page = qb.paginate(page=1, per_page=20) # {"items", "total", ...}
727
727
  ## kit.ai —— AI 多模型服务池
728
728
 
729
729
  安装:`pip install "sdpy-kit[ai]"`
730
+
731
+ 传输(0.6.11):chat / embedding / image 走 `openai` SDK(`>=3.44,<4`,
732
+ 自带 `httpx2`),rerank 走 httpx 自研传输(SDK 无 `/rerank` 资源)。
733
+ 档案里的 `endpoint` **对前三类不再生效**——SDK 固定拼 `/chat/completions`、
734
+ `/embeddings`、`/images/generations`,仅 rerank 与探活仍使用该字段。
735
+
736
+ > 安装提示:`openai` 3.x 会引入 `httpx2`,与 rerank 用的 `httpx` 并存(两个包名不同,不冲突)。
737
+
730
738
  档案:项目根 `config/ai_models.json`(见文末「AI 档案文件」)
731
739
 
732
740
  ### 1. 同步门面 `AI`(推荐)
@@ -1518,7 +1526,8 @@ AI 功能需要在项目根目录提供 `config/ai_models.json`(chat / embeddi
1518
1526
 
1519
1527
  | 层级 | 字段 | 说明 |
1520
1528
  |------|------|------|
1521
- | 档案级 | `timeout` | 请求超时秒数(默认 60) |
1529
+ | 档案级 | `endpoint` | 请求路径。**0.6.10 起仅 rerank 生效**:chat / embedding / image 由 openai SDK 固定拼路径,本字段被忽略(保留仅为兼容旧配置,不报错) |
1530
+ | 档案级 | `timeout` | 请求超时秒数(默认 60);SDK 侧为单次调用总超时 |
1522
1531
  | 档案级 | `max_tokens` | 输出上限(chat 用);`AI.chat_ask/chat_complete/chat_stream` 也可按调用点传参,逐级回落:调用点 → 档案 → provider 默认 |
1523
1532
  | 档案级 | `retries` | 5xx 重试次数(默认 2;超时不重试,候选链切换才是有效重试) |
1524
1533
  | 档案级 | `extra_body` | 厂商专有请求体字段透传(如 GLM/Qwen 关闭思考的 `{"thinking": {"type": "disabled"}}`、`{"reasoning_effort": "low"}`);与服务构造的同名键冲突时**以服务的值为准**,故 `model` / `messages` / `stream` 等保留键不可覆盖;改名类需求(如 OpenAI o 系列的 `max_completion_tokens`)无法用它实现。逐调用 `thinking` 与之同键时**以逐调用为准**并打 `WARNING`。改文件后需 `reload_config(reload_file=True)` 生效 |
@@ -23,7 +23,7 @@ Author: Clark Chang
23
23
  数据库访问通过 kit.db 子包提供(基于 SQLModel)。
24
24
  """
25
25
 
26
- __version__ = "0.6.9"
26
+ __version__ = "0.6.11"
27
27
 
28
28
  from kit.db import (
29
29
  MYSQL,
@@ -0,0 +1,51 @@
1
+ """
2
+ Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+ Project: sdpy
17
+ Author: Clark Chang
18
+ """
19
+
20
+ """``Retry-After`` 响应头解析(SDK 传输与 rerank 传输共用)。"""
21
+
22
+ from datetime import datetime, timezone
23
+ from email.utils import parsedate_to_datetime
24
+ from typing import Final
25
+
26
+ __all__ = ["parse_retry_after"]
27
+
28
+ # 重试前剩余整链预算的最小门槛(秒):不足则放弃重试直接抛出
29
+ MIN_RETRY_BUDGET: Final[float] = 1.0
30
+
31
+
32
+ def parse_retry_after(raw: str | None) -> float | None:
33
+ """解析 ``Retry-After`` 响应头为等待秒数。
34
+
35
+ 支持 RFC 7231 两种格式:整数秒与 HTTP-date(按 UTC 计算剩余秒,
36
+ 过去时刻钳为 0);缺失或非法返回 None。
37
+ """
38
+ if not raw:
39
+ return None
40
+ raw = raw.strip()
41
+ if raw.isdigit():
42
+ return float(raw)
43
+ try:
44
+ dt = parsedate_to_datetime(raw)
45
+ except (TypeError, ValueError):
46
+ return None
47
+ if dt is None:
48
+ return None
49
+ if dt.tzinfo is None:
50
+ dt = dt.replace(tzinfo=timezone.utc)
51
+ return max((dt - datetime.now(timezone.utc)).total_seconds(), 0.0)
@@ -0,0 +1,115 @@
1
+ """
2
+ Copyright (c) 2021-2026 Clark Chang. All Rights Reserved.
3
+
4
+ Licensed under the Apache License, Version 2.0 (the "License");
5
+ you may not use this file except in compliance with the License.
6
+ You may obtain a copy of the License at
7
+
8
+ http://www.apache.org/licenses/LICENSE-2.0
9
+
10
+ Unless required by applicable law or agreed to in writing, software
11
+ distributed under the License is distributed on an "AS IS" BASIS,
12
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ See the License for the specific language governing permissions and
14
+ limitations under the License.
15
+
16
+ Project: sdpy
17
+ Author: Clark Chang
18
+ """
19
+
20
+ # date: 2026-11-04
21
+ """AI 服务公共基类:档案属性、生命周期与统一日志骨架。
22
+
23
+ 0.6.10 起不再包含 HTTP 传输与请求体编排:chat / embedding / image 由
24
+ ``SdkClient`` 承担,rerank 由 ``RerankTransport`` 承担。本模块只保留
25
+ 四类服务共用的日志与资源管理能力。
26
+ """
27
+
28
+ import time
29
+ from abc import ABC
30
+ from collections.abc import Iterator
31
+ from contextlib import contextmanager
32
+ from typing import ClassVar
33
+
34
+ from kit.ai.exceptions import AIError, AIResponseError
35
+ from kit.ai.profiles import ModelProfile
36
+ from kit.ai.types import JSONValue
37
+ from kit.config.log_config import get_default_logger
38
+
39
+ __all__ = ["LoggingServiceBase", "safe_get"]
40
+
41
+ logger = get_default_logger(__name__)
42
+
43
+
44
+ def safe_get(data: JSONValue, *path: str | int) -> JSONValue:
45
+ """按层级路径安全取值,支持 dict 键(str)与 list 索引(int)。
46
+
47
+ Raises:
48
+ AIResponseError: 路径任一环节缺失或类型不符。
49
+ """
50
+ cur: JSONValue = data
51
+ for key in path:
52
+ if isinstance(key, int):
53
+ if not isinstance(cur, list) or key < 0 or key >= len(cur):
54
+ raise AIResponseError(f"响应缺少索引 {path}: {str(data)[:200]}")
55
+ cur = cur[key]
56
+ else:
57
+ if not isinstance(cur, dict) or key not in cur:
58
+ raise AIResponseError(
59
+ f"响应缺少字段 {'.'.join(map(str, path))}: {str(data)[:200]}"
60
+ )
61
+ cur = cur[key]
62
+ return cur
63
+
64
+
65
+ class LoggingServiceBase(ABC):
66
+ """四类模型服务共用的档案属性、日志骨架与异步上下文。"""
67
+
68
+ _LABEL: ClassVar[str] = "模型"
69
+
70
+ def __init__(self, profile: ModelProfile) -> None:
71
+ self._profile: ModelProfile = profile
72
+
73
+ @property
74
+ def profile(self) -> ModelProfile:
75
+ return self._profile
76
+
77
+ def _log_desc(self, *, suffix: str = "") -> str:
78
+ """日志中的「模型及描述」:服务中文名 + 模型标识 + 实例名。"""
79
+ return (
80
+ f"{self._LABEL} {self._profile.model}"
81
+ f"(实例 {self._profile.name}{suffix})"
82
+ )
83
+
84
+ @contextmanager
85
+ def _log_call(self, *, log: bool = True) -> Iterator[None]:
86
+ """统一日志骨架:Start → 成功耗时 / 失败 ERROR。"""
87
+ desc = self._log_desc()
88
+ if log:
89
+ logger.info(f"********** {desc} ********** Start")
90
+ start = time.perf_counter()
91
+ try:
92
+ yield
93
+ except AIError as exc:
94
+ logger.error(
95
+ f"********** {desc} ********** "
96
+ f"耗时 {time.perf_counter() - start:.1f}秒(失败:{exc})"
97
+ )
98
+ raise
99
+ if log:
100
+ logger.info(
101
+ f"********** {desc} ********** 耗时 {time.perf_counter() - start:.1f}秒"
102
+ )
103
+
104
+ async def close(self) -> None:
105
+ """释放底层资源(子类实现)。"""
106
+ raise NotImplementedError
107
+
108
+ async def __aenter__(self) -> "LoggingServiceBase":
109
+ return self
110
+
111
+ async def __aexit__(self, *exc_info: object) -> None:
112
+ await self.close()
113
+
114
+ def __str__(self) -> str:
115
+ return f"{type(self).__name__}({self._profile.name})"