python-basekit 0.0.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.
- python_basekit-0.0.11/PKG-INFO +46 -0
- python_basekit-0.0.11/README.md +19 -0
- python_basekit-0.0.11/pyproject.toml +92 -0
- python_basekit-0.0.11/src/basekit/__init__.py +0 -0
- python_basekit-0.0.11/src/basekit/ai/clients/anthropic.py +192 -0
- python_basekit-0.0.11/src/basekit/ai/clients/dashscope.py +133 -0
- python_basekit-0.0.11/src/basekit/ai/clients/gemini.py +277 -0
- python_basekit-0.0.11/src/basekit/ai/clients/openai.py +384 -0
- python_basekit-0.0.11/src/basekit/ai/schema.py +160 -0
- python_basekit-0.0.11/src/basekit/ai/utils.py +63 -0
- python_basekit-0.0.11/src/basekit/cache/clients/sqlite.py +90 -0
- python_basekit-0.0.11/src/basekit/cache/schema.py +53 -0
- python_basekit-0.0.11/src/basekit/cache/utils.py +113 -0
- python_basekit-0.0.11/src/basekit/database.py +33 -0
- python_basekit-0.0.11/src/basekit/http/clients/curl_cffi.py +99 -0
- python_basekit-0.0.11/src/basekit/http/clients/httpx.py +100 -0
- python_basekit-0.0.11/src/basekit/http/schema.py +111 -0
- python_basekit-0.0.11/src/basekit/http/utils.py +31 -0
- python_basekit-0.0.11/src/basekit/limiter.py +179 -0
- python_basekit-0.0.11/src/basekit/py.typed +0 -0
- python_basekit-0.0.11/src/basekit/utils/batch.py +21 -0
- python_basekit-0.0.11/src/basekit/utils/console.py +26 -0
- python_basekit-0.0.11/src/basekit/utils/html.py +54 -0
- python_basekit-0.0.11/src/basekit/utils/jinja.py +20 -0
- python_basekit-0.0.11/src/basekit/utils/markdown.py +38 -0
- python_basekit-0.0.11/src/basekit/utils/mime.py +39 -0
- python_basekit-0.0.11/src/basekit/utils/misc.py +35 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: python-basekit
|
|
3
|
+
Version: 0.0.11
|
|
4
|
+
Summary: Python Base Kit
|
|
5
|
+
Author: YanSH
|
|
6
|
+
Author-email: YanSH <yansh97@foxmail.com>
|
|
7
|
+
Requires-Dist: aiosqlite>=0.22.1
|
|
8
|
+
Requires-Dist: beautifulsoup4>=4.14.3
|
|
9
|
+
Requires-Dist: curl-cffi>=0.14.0
|
|
10
|
+
Requires-Dist: filetype>=1.2.0
|
|
11
|
+
Requires-Dist: flowmark>=0.6.5
|
|
12
|
+
Requires-Dist: greenlet>=3.3.2
|
|
13
|
+
Requires-Dist: httpx[http2]>=0.28.1
|
|
14
|
+
Requires-Dist: jinja2>=3.1.6
|
|
15
|
+
Requires-Dist: logfire>=4.27.0
|
|
16
|
+
Requires-Dist: lxml>=6.0.2
|
|
17
|
+
Requires-Dist: markdown-it-py>=4.0.0
|
|
18
|
+
Requires-Dist: markdownify>=1.2.2
|
|
19
|
+
Requires-Dist: pydantic>=2.12.5
|
|
20
|
+
Requires-Dist: pydantic-settings>=2.13.1
|
|
21
|
+
Requires-Dist: python-dotenv>=1.2.2
|
|
22
|
+
Requires-Dist: rich>=14.3.3
|
|
23
|
+
Requires-Dist: sqlalchemy>=2.0.48
|
|
24
|
+
Requires-Dist: tenacity>=9.1.4
|
|
25
|
+
Requires-Python: >=3.12
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Base Kit
|
|
29
|
+
|
|
30
|
+
一个面向内部使用的 Python 基础工具包。
|
|
31
|
+
|
|
32
|
+
## 环境
|
|
33
|
+
|
|
34
|
+
Python `>=3.12`
|
|
35
|
+
|
|
36
|
+
## 安装
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
uv add python-basekit
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 使用
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
import basekit
|
|
46
|
+
```
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "python-basekit"
|
|
3
|
+
version = "0.0.11"
|
|
4
|
+
description = "Python Base Kit"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
authors = [{ name = "YanSH", email = "yansh97@foxmail.com" }]
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = [
|
|
9
|
+
"aiosqlite>=0.22.1",
|
|
10
|
+
"beautifulsoup4>=4.14.3",
|
|
11
|
+
"curl-cffi>=0.14.0",
|
|
12
|
+
"filetype>=1.2.0",
|
|
13
|
+
"flowmark>=0.6.5",
|
|
14
|
+
"greenlet>=3.3.2",
|
|
15
|
+
"httpx[http2]>=0.28.1",
|
|
16
|
+
"jinja2>=3.1.6",
|
|
17
|
+
"logfire>=4.27.0",
|
|
18
|
+
"lxml>=6.0.2",
|
|
19
|
+
"markdown-it-py>=4.0.0",
|
|
20
|
+
"markdownify>=1.2.2",
|
|
21
|
+
"pydantic>=2.12.5",
|
|
22
|
+
"pydantic-settings>=2.13.1",
|
|
23
|
+
"python-dotenv>=1.2.2",
|
|
24
|
+
"rich>=14.3.3",
|
|
25
|
+
"sqlalchemy>=2.0.48",
|
|
26
|
+
"tenacity>=9.1.4",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[dependency-groups]
|
|
30
|
+
dev = ["pytest>=9.0.2", "pytest-asyncio>=1.3.0", "ruff>=0.15.5"]
|
|
31
|
+
|
|
32
|
+
[build-system]
|
|
33
|
+
requires = ["uv_build>=0.11.10,<0.12.0"]
|
|
34
|
+
build-backend = "uv_build"
|
|
35
|
+
|
|
36
|
+
[tool.uv]
|
|
37
|
+
environments = ["sys_platform == 'darwin'", "sys_platform == 'linux'"]
|
|
38
|
+
|
|
39
|
+
[tool.uv.build-backend]
|
|
40
|
+
module-name = "basekit"
|
|
41
|
+
|
|
42
|
+
# ruff 配置
|
|
43
|
+
[tool.ruff]
|
|
44
|
+
cache-dir = ".cache/.ruff_cache"
|
|
45
|
+
|
|
46
|
+
# pytest 配置
|
|
47
|
+
[tool.pytest.ini_options]
|
|
48
|
+
|
|
49
|
+
# 测试文件查找路径
|
|
50
|
+
testpaths = ["tests"]
|
|
51
|
+
|
|
52
|
+
# 测试文件/类/函数的命名规则
|
|
53
|
+
python_files = ["test_*.py", "*_test.py"]
|
|
54
|
+
python_classes = ["Test*"]
|
|
55
|
+
python_functions = ["test_*"]
|
|
56
|
+
|
|
57
|
+
# pytest 缓存目录统一到 .cache/
|
|
58
|
+
cache_dir = ".cache/.pytest_cache"
|
|
59
|
+
|
|
60
|
+
# 异步测试配置
|
|
61
|
+
asyncio_mode = "auto"
|
|
62
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
63
|
+
asyncio_default_test_loop_scope = "function"
|
|
64
|
+
|
|
65
|
+
# Logfire 测试配置
|
|
66
|
+
logfire = false
|
|
67
|
+
|
|
68
|
+
# pytest 命令行默认选项
|
|
69
|
+
addopts = "-ra --showlocals --strict-markers --strict-config --tb=auto --import-mode=importlib"
|
|
70
|
+
|
|
71
|
+
# 测试标记
|
|
72
|
+
markers = [
|
|
73
|
+
"zero_cost: 无成本测试,不调用 AI 实际接口",
|
|
74
|
+
"low_cost: 低成本测试,仅 create_message 相关",
|
|
75
|
+
"high_cost: 高成本测试,如 structured/tool_use/image 等",
|
|
76
|
+
]
|
|
77
|
+
|
|
78
|
+
# 警告视为错误,早发现潜在问题
|
|
79
|
+
filterwarnings = "error"
|
|
80
|
+
|
|
81
|
+
# 进度条风格
|
|
82
|
+
console_output_style = "progress"
|
|
83
|
+
|
|
84
|
+
# pyright 配置
|
|
85
|
+
[tool.pyright]
|
|
86
|
+
|
|
87
|
+
# 与项目运行时版本保持一致,避免误报语法不支持
|
|
88
|
+
pythonVersion = "3.12"
|
|
89
|
+
|
|
90
|
+
# 虚拟环境路径
|
|
91
|
+
venvPath = "."
|
|
92
|
+
venv = ".venv"
|
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import ClassVar, override
|
|
3
|
+
|
|
4
|
+
import logfire
|
|
5
|
+
from httpx import Response
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from basekit.ai.schema import AIClient, Tool, Usage, ValidationError
|
|
9
|
+
from basekit.ai.utils import transform_schema
|
|
10
|
+
from basekit.utils.misc import recursive_merge
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AnthropicClient(AIClient):
|
|
14
|
+
cache_key_fields: ClassVar[tuple[str, ...]] = ("max_tokens",)
|
|
15
|
+
|
|
16
|
+
api_key: str
|
|
17
|
+
base_url: str = "https://api.anthropic.com"
|
|
18
|
+
max_tokens: int = 16000
|
|
19
|
+
|
|
20
|
+
def _headers(self) -> dict:
|
|
21
|
+
return {
|
|
22
|
+
"X-Api-Key": self.api_key,
|
|
23
|
+
"Anthropic-Version": "2023-06-01",
|
|
24
|
+
"Content-Type": "application/json",
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
def _message_url(self) -> str:
|
|
28
|
+
return f"{self.base_url}/v1/messages"
|
|
29
|
+
|
|
30
|
+
def _message_params(self, system: str, prompt: str, /) -> dict:
|
|
31
|
+
return {
|
|
32
|
+
"model": self.model.request_name,
|
|
33
|
+
"system": system,
|
|
34
|
+
"messages": [{"role": "user", "content": prompt}],
|
|
35
|
+
"max_tokens": self.max_tokens,
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def _structured_message_params(self, schema: dict, /) -> dict:
|
|
39
|
+
return {"output_config": {"format": {"type": "json_schema", "schema": schema}}}
|
|
40
|
+
|
|
41
|
+
def _tool_use_params(self, tool: dict, /) -> dict:
|
|
42
|
+
return {"tools": [tool], "tool_choice": {"type": "any"}}
|
|
43
|
+
|
|
44
|
+
def _get_block(
|
|
45
|
+
self, data: dict, stop_reason: str, filter_: Callable[[dict], bool], /
|
|
46
|
+
) -> dict:
|
|
47
|
+
if data["stop_reason"] != stop_reason:
|
|
48
|
+
raise ValueError(f"STOP REASON:{data['stop_reason']}")
|
|
49
|
+
|
|
50
|
+
blocks: list[dict] = [c for c in data["content"] if filter_(c)]
|
|
51
|
+
if len(blocks) != 1:
|
|
52
|
+
raise ValueError(f"LEN(BLOCKS):{len(blocks)}")
|
|
53
|
+
|
|
54
|
+
return blocks[0]
|
|
55
|
+
|
|
56
|
+
def _get_usage(self, data: dict, /) -> Usage:
|
|
57
|
+
usage: dict = data["usage"]
|
|
58
|
+
cache_creation_tokens: int = usage.get("cache_creation_input_tokens", 0)
|
|
59
|
+
cache_read_tokens: int = usage.get("cache_read_input_tokens", 0)
|
|
60
|
+
return Usage(
|
|
61
|
+
input_tokens=usage["input_tokens"]
|
|
62
|
+
+ cache_creation_tokens
|
|
63
|
+
+ cache_read_tokens,
|
|
64
|
+
cache_creation_tokens=cache_creation_tokens,
|
|
65
|
+
cache_read_tokens=cache_read_tokens,
|
|
66
|
+
output_tokens=usage["output_tokens"],
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
async def _create_message_once(
|
|
71
|
+
self, system: str, prompt: str, extra: dict | None
|
|
72
|
+
) -> str:
|
|
73
|
+
with logfire.span("anthropic client | create message") as span:
|
|
74
|
+
span.set_attribute(key="model", value=self.model)
|
|
75
|
+
span.set_attribute(key="system", value=system)
|
|
76
|
+
span.set_attribute(key="prompt", value=prompt)
|
|
77
|
+
span.set_attribute(key="extra", value=extra)
|
|
78
|
+
|
|
79
|
+
async with self._limiter:
|
|
80
|
+
response: Response = await self._http_client.post(
|
|
81
|
+
self._message_url(),
|
|
82
|
+
headers=self._headers(),
|
|
83
|
+
data=recursive_merge(
|
|
84
|
+
self._message_params(system, prompt),
|
|
85
|
+
self.model.extra_params,
|
|
86
|
+
extra,
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
try:
|
|
91
|
+
data: dict = response.json()
|
|
92
|
+
block: dict = self._get_block(
|
|
93
|
+
data, "end_turn", lambda x: x["type"] == "text"
|
|
94
|
+
)
|
|
95
|
+
message: str = block["text"]
|
|
96
|
+
usage: Usage = self._get_usage(data)
|
|
97
|
+
|
|
98
|
+
span.set_attribute(key="return", value=message)
|
|
99
|
+
span.set_attribute(key="usage", value=usage)
|
|
100
|
+
span.message = f"{span.message} -> {usage.format()} tokens"
|
|
101
|
+
return message
|
|
102
|
+
except Exception as exc:
|
|
103
|
+
raise ValidationError("生成消息失败") from exc
|
|
104
|
+
|
|
105
|
+
@override
|
|
106
|
+
async def _create_structured_message_once[T: BaseModel](
|
|
107
|
+
self, system: str, prompt: str, schema: type[T], extra: dict | None
|
|
108
|
+
) -> T:
|
|
109
|
+
schema_name: str = schema.__name__
|
|
110
|
+
with logfire.span(
|
|
111
|
+
f"anthropic client | create structured message | {schema_name}"
|
|
112
|
+
) as span:
|
|
113
|
+
schema_data: dict = transform_schema(schema.model_json_schema())
|
|
114
|
+
|
|
115
|
+
span.set_attribute(key="model", value=self.model)
|
|
116
|
+
span.set_attribute(key="system", value=system)
|
|
117
|
+
span.set_attribute(key="prompt", value=prompt)
|
|
118
|
+
span.set_attribute(key="schema", value=schema_data)
|
|
119
|
+
span.set_attribute(key="extra", value=extra)
|
|
120
|
+
|
|
121
|
+
async with self._limiter:
|
|
122
|
+
response: Response = await self._http_client.post(
|
|
123
|
+
self._message_url(),
|
|
124
|
+
headers=self._headers(),
|
|
125
|
+
data=recursive_merge(
|
|
126
|
+
self._message_params(system, prompt),
|
|
127
|
+
self._structured_message_params(schema_data),
|
|
128
|
+
self.model.extra_params,
|
|
129
|
+
extra,
|
|
130
|
+
),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
try:
|
|
134
|
+
data: dict = response.json()
|
|
135
|
+
block: dict = self._get_block(
|
|
136
|
+
data, "end_turn", lambda x: x["type"] == "text"
|
|
137
|
+
)
|
|
138
|
+
result: T = schema.model_validate_json(json_data=block["text"])
|
|
139
|
+
usage: Usage = self._get_usage(data)
|
|
140
|
+
|
|
141
|
+
span.set_attribute(key="return", value=result)
|
|
142
|
+
span.set_attribute(key="usage", value=usage)
|
|
143
|
+
span.message = f"{span.message} -> {usage.format()} tokens"
|
|
144
|
+
return result
|
|
145
|
+
except Exception as exc:
|
|
146
|
+
raise ValidationError("生成结构化消息失败") from exc
|
|
147
|
+
|
|
148
|
+
@override
|
|
149
|
+
async def _create_tool_use_once[T: BaseModel](
|
|
150
|
+
self, system: str, prompt: str, tool: Tool[T], extra: dict | None
|
|
151
|
+
) -> T:
|
|
152
|
+
tool_name: str = tool.name
|
|
153
|
+
with logfire.span(f"anthropic client | create tool use | {tool_name}") as span:
|
|
154
|
+
tool_: dict = {
|
|
155
|
+
"name": tool.name,
|
|
156
|
+
"description": tool.description,
|
|
157
|
+
"input_schema": transform_schema(tool.input_schema.model_json_schema()),
|
|
158
|
+
"strict": True,
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
span.set_attribute(key="model", value=self.model)
|
|
162
|
+
span.set_attribute(key="system", value=system)
|
|
163
|
+
span.set_attribute(key="prompt", value=prompt)
|
|
164
|
+
span.set_attribute(key="tool", value=tool_)
|
|
165
|
+
span.set_attribute(key="extra", value=extra)
|
|
166
|
+
|
|
167
|
+
async with self._limiter:
|
|
168
|
+
response: Response = await self._http_client.post(
|
|
169
|
+
self._message_url(),
|
|
170
|
+
headers=self._headers(),
|
|
171
|
+
data=recursive_merge(
|
|
172
|
+
self._message_params(system, prompt),
|
|
173
|
+
self._tool_use_params(tool_),
|
|
174
|
+
self.model.extra_params,
|
|
175
|
+
extra,
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try:
|
|
180
|
+
data: dict = response.json()
|
|
181
|
+
block: dict = self._get_block(
|
|
182
|
+
data, "tool_use", lambda x: x["type"] == "tool_use"
|
|
183
|
+
)
|
|
184
|
+
result: T = tool.input_schema.model_validate(obj=block["input"])
|
|
185
|
+
usage: Usage = self._get_usage(data)
|
|
186
|
+
|
|
187
|
+
span.set_attribute(key="return", value=result)
|
|
188
|
+
span.set_attribute(key="usage", value=usage)
|
|
189
|
+
span.message = f"{span.message} -> {usage.format()} tokens"
|
|
190
|
+
return result
|
|
191
|
+
except Exception as exc:
|
|
192
|
+
raise ValidationError("生成工具调用失败") from exc
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from typing import ClassVar, override
|
|
3
|
+
|
|
4
|
+
import logfire
|
|
5
|
+
from httpx import Response
|
|
6
|
+
from pydantic import BaseModel
|
|
7
|
+
|
|
8
|
+
from basekit.ai.schema import (
|
|
9
|
+
AIClient,
|
|
10
|
+
ImageRatio,
|
|
11
|
+
ImageSize,
|
|
12
|
+
NotSupportedError,
|
|
13
|
+
ValidationError,
|
|
14
|
+
)
|
|
15
|
+
from basekit.utils.misc import recursive_merge
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class ImageUsage(BaseModel):
|
|
19
|
+
width: int
|
|
20
|
+
height: int
|
|
21
|
+
|
|
22
|
+
def format(self) -> str:
|
|
23
|
+
return f"{self.width}x{self.height}"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class DashScopeClient(AIClient):
|
|
27
|
+
cache_key_fields: ClassVar[tuple[str, ...]] = ()
|
|
28
|
+
|
|
29
|
+
api_key: str
|
|
30
|
+
base_url: str = "https://dashscope.aliyuncs.com/api/v1"
|
|
31
|
+
|
|
32
|
+
def _headers(self) -> dict:
|
|
33
|
+
return {
|
|
34
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
35
|
+
"Content-Type": "application/json",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
def _image_url(self) -> str:
|
|
39
|
+
return f"{self.base_url}/services/aigc/multimodal-generation/generation"
|
|
40
|
+
|
|
41
|
+
def _image_resolution(self, ratio: ImageRatio, size: ImageSize, /) -> str:
|
|
42
|
+
base_resolution_map: dict[ImageRatio, tuple[int, int]] = {
|
|
43
|
+
"9:16": (768, 1344),
|
|
44
|
+
"2:3": (832, 1248),
|
|
45
|
+
"3:4": (864, 1184),
|
|
46
|
+
"4:5": (896, 1152),
|
|
47
|
+
"1:1": (1024, 1024),
|
|
48
|
+
"5:4": (1152, 896),
|
|
49
|
+
"4:3": (1184, 864),
|
|
50
|
+
"3:2": (1248, 832),
|
|
51
|
+
"16:9": (1344, 768),
|
|
52
|
+
}
|
|
53
|
+
resolution_ratio_map: dict[ImageSize, int] = {"1K": 1, "2K": 2, "4K": 4}
|
|
54
|
+
|
|
55
|
+
width: int = base_resolution_map[ratio][0] * resolution_ratio_map[size]
|
|
56
|
+
height: int = base_resolution_map[ratio][1] * resolution_ratio_map[size]
|
|
57
|
+
resolution: str = f"{width}*{height}"
|
|
58
|
+
|
|
59
|
+
match self.model.name:
|
|
60
|
+
case "qwen-image-2.0" | "qwen-image-2.0-pro":
|
|
61
|
+
if size not in {"1K", "2K"}:
|
|
62
|
+
raise NotSupportedError(
|
|
63
|
+
f"模型 {self.model.name} 仅支持 1K 和 2K 分辨率"
|
|
64
|
+
)
|
|
65
|
+
return resolution
|
|
66
|
+
case _:
|
|
67
|
+
raise NotSupportedError(f"模型 {self.model.name} 不合法")
|
|
68
|
+
|
|
69
|
+
def _image_params(self, prompt: str, resolution: str, /) -> dict:
|
|
70
|
+
return {
|
|
71
|
+
"model": self.model.request_name,
|
|
72
|
+
"input": {"messages": [{"role": "user", "content": [{"text": prompt}]}]},
|
|
73
|
+
"parameters": {"size": resolution},
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
def _get_block(self, data: dict, filter_: Callable[[dict], bool], /) -> dict:
|
|
77
|
+
choices: list = data["output"]["choices"]
|
|
78
|
+
if len(choices) != 1:
|
|
79
|
+
raise ValueError(f"LEN(CHOICES):{len(choices)}")
|
|
80
|
+
|
|
81
|
+
choice: dict = choices[0]
|
|
82
|
+
if choice["finish_reason"] != "stop":
|
|
83
|
+
raise ValueError(f"FINISH REASON:{choice['finish_reason']}")
|
|
84
|
+
|
|
85
|
+
blocks: list[dict] = [c for c in choice["message"]["content"] if filter_(c)]
|
|
86
|
+
if not blocks:
|
|
87
|
+
raise ValueError(f"LEN(BLOCKS):{len(blocks)}")
|
|
88
|
+
|
|
89
|
+
return blocks[0]
|
|
90
|
+
|
|
91
|
+
def _get_usage(self, data: dict, /) -> ImageUsage:
|
|
92
|
+
usage: dict = data["usage"]
|
|
93
|
+
return ImageUsage(width=usage["width"], height=usage["height"])
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
async def _create_image_once(
|
|
97
|
+
self, prompt: str, ratio: ImageRatio, size: ImageSize, extra: dict | None
|
|
98
|
+
) -> bytes:
|
|
99
|
+
with logfire.span("dashscope client | create image") as span:
|
|
100
|
+
resolution: str = self._image_resolution(ratio, size)
|
|
101
|
+
|
|
102
|
+
span.set_attribute(key="model", value=self.model)
|
|
103
|
+
span.set_attribute(key="prompt", value=prompt)
|
|
104
|
+
span.set_attribute(key="ratio", value=ratio)
|
|
105
|
+
span.set_attribute(key="size", value=size)
|
|
106
|
+
span.set_attribute(key="extra", value=extra)
|
|
107
|
+
span.set_attribute(key="resolution", value=resolution)
|
|
108
|
+
|
|
109
|
+
async with self._limiter:
|
|
110
|
+
response: Response = await self._http_client.post(
|
|
111
|
+
self._image_url(),
|
|
112
|
+
headers=self._headers(),
|
|
113
|
+
data=recursive_merge(
|
|
114
|
+
self._image_params(prompt, resolution),
|
|
115
|
+
self.model.extra_params,
|
|
116
|
+
extra,
|
|
117
|
+
),
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
try:
|
|
121
|
+
data: dict = response.json()
|
|
122
|
+
block: dict = self._get_block(data, lambda c: "image" in c)
|
|
123
|
+
image_url: str = block["image"]
|
|
124
|
+
image: bytes = (await self._http_client.get(image_url)).content
|
|
125
|
+
usage: ImageUsage = self._get_usage(data)
|
|
126
|
+
|
|
127
|
+
span.set_attribute(key="image_url", value=image_url)
|
|
128
|
+
span.set_attribute(key="filesize", value=len(image))
|
|
129
|
+
span.set_attribute(key="usage", value=usage)
|
|
130
|
+
span.message = f"{span.message} -> {usage.format()} pixels"
|
|
131
|
+
return image
|
|
132
|
+
except Exception as exc:
|
|
133
|
+
raise ValidationError("生成图像失败") from exc
|