ark-utils-cli 0.3.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ark_cli/__init__.py +3 -0
- ark_cli/cli.py +268 -0
- ark_cli/model_client.py +90 -0
- ark_cli/search_client.py +126 -0
- ark_cli/tos_client.py +185 -0
- ark_utils_cli-0.3.0.dist-info/METADATA +277 -0
- ark_utils_cli-0.3.0.dist-info/RECORD +10 -0
- ark_utils_cli-0.3.0.dist-info/WHEEL +4 -0
- ark_utils_cli-0.3.0.dist-info/entry_points.txt +2 -0
- ark_utils_cli-0.3.0.dist-info/licenses/LICENSE +21 -0
ark_cli/__init__.py
ADDED
ark_cli/cli.py
ADDED
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
"""命令行入口"""
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from ark_cli import __version__
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def handle_tos(args):
|
|
10
|
+
"""处理 TOS 相关命令"""
|
|
11
|
+
from ark_cli.tos_client import TosClient
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
client = TosClient()
|
|
15
|
+
except ValueError as e:
|
|
16
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
17
|
+
sys.exit(1)
|
|
18
|
+
|
|
19
|
+
if args.action == "upload":
|
|
20
|
+
if not all([args.bucket, args.input, args.output]):
|
|
21
|
+
print("错误: upload 操作需要 -bucket, -i, -o 参数", file=sys.stderr)
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
try:
|
|
25
|
+
url = client.upload(args.bucket, args.input, args.output)
|
|
26
|
+
print(url)
|
|
27
|
+
except FileNotFoundError as e:
|
|
28
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
29
|
+
sys.exit(1)
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"上传失败: {e}", file=sys.stderr)
|
|
32
|
+
sys.exit(1)
|
|
33
|
+
|
|
34
|
+
elif args.action == "ls":
|
|
35
|
+
if not args.bucket:
|
|
36
|
+
print("错误: ls 操作需要 -bucket 参数", file=sys.stderr)
|
|
37
|
+
sys.exit(1)
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
prefix = args.input or ""
|
|
41
|
+
# TOS 对象 key 不以 / 开头,/ 表示根目录
|
|
42
|
+
if prefix == "/":
|
|
43
|
+
prefix = ""
|
|
44
|
+
# 移除开头的 /
|
|
45
|
+
prefix = prefix.lstrip("/")
|
|
46
|
+
items = client.list_objects(args.bucket, prefix=prefix)
|
|
47
|
+
|
|
48
|
+
if not items:
|
|
49
|
+
print("(空)")
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
# 格式化输出
|
|
53
|
+
for item in items:
|
|
54
|
+
if item["type"] == "dir":
|
|
55
|
+
print(f"{'DIR':<10} {'-':<20} {item['key']}")
|
|
56
|
+
else:
|
|
57
|
+
size_str = _format_size(item["size"])
|
|
58
|
+
print(f"{size_str:<10} {item['last_modified']:<20} {item['key']}")
|
|
59
|
+
except Exception as e:
|
|
60
|
+
print(f"列表失败: {e}", file=sys.stderr)
|
|
61
|
+
sys.exit(1)
|
|
62
|
+
|
|
63
|
+
elif args.action == "geturl":
|
|
64
|
+
if not all([args.bucket, args.input]):
|
|
65
|
+
print("错误: geturl 操作需要 -bucket, -i 参数", file=sys.stderr)
|
|
66
|
+
sys.exit(1)
|
|
67
|
+
|
|
68
|
+
try:
|
|
69
|
+
url = client.get_url(args.bucket, args.input)
|
|
70
|
+
print(url)
|
|
71
|
+
except Exception as e:
|
|
72
|
+
print(f"获取链接失败: {e}", file=sys.stderr)
|
|
73
|
+
sys.exit(1)
|
|
74
|
+
|
|
75
|
+
elif args.action == "lsb":
|
|
76
|
+
try:
|
|
77
|
+
items = client.list_buckets()
|
|
78
|
+
|
|
79
|
+
if not items:
|
|
80
|
+
print("(空)")
|
|
81
|
+
return
|
|
82
|
+
|
|
83
|
+
for item in items:
|
|
84
|
+
print(f"{item['creation_date']:<20} {item['name']}")
|
|
85
|
+
except Exception as e:
|
|
86
|
+
print(f"列表失败: {e}", file=sys.stderr)
|
|
87
|
+
sys.exit(1)
|
|
88
|
+
|
|
89
|
+
elif args.action == "rm":
|
|
90
|
+
if not all([args.bucket, args.input]):
|
|
91
|
+
print("错误: rm 操作需要 -bucket, -i 参数", file=sys.stderr)
|
|
92
|
+
sys.exit(1)
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
client.delete(args.bucket, args.input)
|
|
96
|
+
print(f"已删除: {args.input}")
|
|
97
|
+
except Exception as e:
|
|
98
|
+
print(f"删除失败: {e}", file=sys.stderr)
|
|
99
|
+
sys.exit(1)
|
|
100
|
+
|
|
101
|
+
elif args.action == "stats":
|
|
102
|
+
if not args.bucket:
|
|
103
|
+
print("错误: stats 操作需要 -bucket 参数", file=sys.stderr)
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
|
|
106
|
+
try:
|
|
107
|
+
info = client.stats(args.bucket)
|
|
108
|
+
print(f"Bucket: {info['name']}")
|
|
109
|
+
print(f"Location: {info['location']}")
|
|
110
|
+
print(f"Created: {info['creation_date']}")
|
|
111
|
+
print(f"Storage: {info['storage_class']}")
|
|
112
|
+
print(f"Redundancy: {info['az_redundancy']}")
|
|
113
|
+
print(f"Versioning: {info['versioning']}")
|
|
114
|
+
print(f"Endpoint: {info['extranet_endpoint']}")
|
|
115
|
+
except Exception as e:
|
|
116
|
+
print(f"获取统计失败: {e}", file=sys.stderr)
|
|
117
|
+
sys.exit(1)
|
|
118
|
+
|
|
119
|
+
else:
|
|
120
|
+
print(f"错误: 不支持的操作: {args.action}", file=sys.stderr)
|
|
121
|
+
sys.exit(1)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _format_size(size: int) -> str:
|
|
125
|
+
"""格式化文件大小"""
|
|
126
|
+
for unit in ["B", "KB", "MB", "GB", "TB"]:
|
|
127
|
+
if size < 1024:
|
|
128
|
+
return f"{size:.1f}{unit}" if unit != "B" else f"{size}{unit}"
|
|
129
|
+
size /= 1024
|
|
130
|
+
return f"{size:.1f}PB"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def handle_model(args):
|
|
134
|
+
"""处理大模型相关命令"""
|
|
135
|
+
from ark_cli.model_client import ModelClient
|
|
136
|
+
|
|
137
|
+
if not all([args.model, args.content]):
|
|
138
|
+
print("错误: model 产品需要 -m 和 -c 参数", file=sys.stderr)
|
|
139
|
+
sys.exit(1)
|
|
140
|
+
|
|
141
|
+
try:
|
|
142
|
+
client = ModelClient()
|
|
143
|
+
result = client.chat(args.model, args.content)
|
|
144
|
+
print(result.content)
|
|
145
|
+
print()
|
|
146
|
+
print(f"[耗时: {result.duration_ms}ms | tokens: {result.input_tokens} in / {result.output_tokens} out]")
|
|
147
|
+
except ValueError as e:
|
|
148
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
149
|
+
sys.exit(1)
|
|
150
|
+
except Exception as e:
|
|
151
|
+
print(f"调用失败: {e}", file=sys.stderr)
|
|
152
|
+
sys.exit(1)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def handle_search(args):
|
|
156
|
+
"""处理联网搜索命令"""
|
|
157
|
+
from ark_cli.search_client import SearchClient
|
|
158
|
+
|
|
159
|
+
if not args.query:
|
|
160
|
+
print("错误: search 产品需要 -q 参数", file=sys.stderr)
|
|
161
|
+
sys.exit(1)
|
|
162
|
+
|
|
163
|
+
try:
|
|
164
|
+
client = SearchClient()
|
|
165
|
+
result = client.search(
|
|
166
|
+
query=args.query,
|
|
167
|
+
count=args.count or 10,
|
|
168
|
+
time_range=args.time_range,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
if not result.results:
|
|
172
|
+
print("未找到相关结果")
|
|
173
|
+
return
|
|
174
|
+
|
|
175
|
+
for i, item in enumerate(result.results, 1):
|
|
176
|
+
print(f"[{i}] {item.title}")
|
|
177
|
+
print(f" {item.site_name} | {item.publish_time[:10] if item.publish_time else '-'}")
|
|
178
|
+
print(f" {item.url}")
|
|
179
|
+
if item.snippet:
|
|
180
|
+
# 截取摘要前 150 字符
|
|
181
|
+
snippet = item.snippet[:150] + "..." if len(item.snippet) > 150 else item.snippet
|
|
182
|
+
print(f" {snippet}")
|
|
183
|
+
print()
|
|
184
|
+
|
|
185
|
+
print(f"[共 {result.result_count} 条结果 | 耗时: {result.duration_ms}ms]")
|
|
186
|
+
|
|
187
|
+
except ValueError as e:
|
|
188
|
+
print(f"错误: {e}", file=sys.stderr)
|
|
189
|
+
sys.exit(1)
|
|
190
|
+
except Exception as e:
|
|
191
|
+
print(f"搜索失败: {e}", file=sys.stderr)
|
|
192
|
+
sys.exit(1)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def main():
|
|
196
|
+
parser = argparse.ArgumentParser(
|
|
197
|
+
prog="ark-cli",
|
|
198
|
+
description=f"火山引擎命令行工具 v{__version__} - 让人类和 AI Agent 都能在终端中操作火山引擎",
|
|
199
|
+
)
|
|
200
|
+
parser.add_argument(
|
|
201
|
+
"-v", "--version",
|
|
202
|
+
action="version",
|
|
203
|
+
version=f"%(prog)s {__version__}",
|
|
204
|
+
)
|
|
205
|
+
parser.add_argument(
|
|
206
|
+
"-p", "--product",
|
|
207
|
+
choices=["tos", "model", "search"],
|
|
208
|
+
help="产品名称",
|
|
209
|
+
)
|
|
210
|
+
parser.add_argument(
|
|
211
|
+
"-a", "--action",
|
|
212
|
+
choices=["upload", "ls", "lsb", "geturl", "rm", "stats"],
|
|
213
|
+
help="操作类型",
|
|
214
|
+
)
|
|
215
|
+
parser.add_argument(
|
|
216
|
+
"-bucket",
|
|
217
|
+
help="bucket 名称",
|
|
218
|
+
)
|
|
219
|
+
parser.add_argument(
|
|
220
|
+
"-i", "--input",
|
|
221
|
+
help="路径(本地文件或云上对象 key)",
|
|
222
|
+
)
|
|
223
|
+
parser.add_argument(
|
|
224
|
+
"-o", "--output",
|
|
225
|
+
help="云上对象 key(仅 upload 使用)",
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"-m", "--model",
|
|
229
|
+
help="模型名称 (例如: doubao-seed-2-0-mini-260215)",
|
|
230
|
+
)
|
|
231
|
+
parser.add_argument(
|
|
232
|
+
"-c", "--content",
|
|
233
|
+
help="对话内容",
|
|
234
|
+
)
|
|
235
|
+
parser.add_argument(
|
|
236
|
+
"-q", "--query",
|
|
237
|
+
help="搜索关键词",
|
|
238
|
+
)
|
|
239
|
+
parser.add_argument(
|
|
240
|
+
"--count",
|
|
241
|
+
type=int,
|
|
242
|
+
default=10,
|
|
243
|
+
help="搜索返回条数 (默认: 10, 最多: 50)",
|
|
244
|
+
)
|
|
245
|
+
parser.add_argument(
|
|
246
|
+
"--time-range",
|
|
247
|
+
choices=["OneDay", "OneWeek", "OneMonth", "OneYear"],
|
|
248
|
+
default="OneWeek",
|
|
249
|
+
help="搜索时间范围 (默认: OneWeek)",
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
args = parser.parse_args()
|
|
253
|
+
|
|
254
|
+
if args.product == "tos":
|
|
255
|
+
handle_tos(args)
|
|
256
|
+
elif args.product == "model":
|
|
257
|
+
handle_model(args)
|
|
258
|
+
elif args.product == "search":
|
|
259
|
+
handle_search(args)
|
|
260
|
+
elif args.product:
|
|
261
|
+
print(f"错误: 不支持的产品: {args.product}", file=sys.stderr)
|
|
262
|
+
sys.exit(1)
|
|
263
|
+
else:
|
|
264
|
+
parser.print_help()
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
if __name__ == "__main__":
|
|
268
|
+
main()
|
ark_cli/model_client.py
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""大模型客户端"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import urllib.request
|
|
9
|
+
import urllib.error
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class ChatResult:
|
|
15
|
+
"""对话结果"""
|
|
16
|
+
|
|
17
|
+
content: str
|
|
18
|
+
duration_ms: int
|
|
19
|
+
input_tokens: int
|
|
20
|
+
output_tokens: int
|
|
21
|
+
|
|
22
|
+
def __str__(self) -> str:
|
|
23
|
+
return self.content
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class ModelClient:
|
|
27
|
+
"""火山引擎大模型客户端"""
|
|
28
|
+
|
|
29
|
+
DEFAULT_ENDPOINT = "https://ark.cn-beijing.volces.com/api/v3/chat/completions"
|
|
30
|
+
|
|
31
|
+
def __init__(self, api_key: str | None = None, endpoint: str | None = None):
|
|
32
|
+
self.api_key = api_key or os.environ.get("ARK_API_KEY")
|
|
33
|
+
self.endpoint = endpoint or os.environ.get("ARK_ENDPOINT") or self.DEFAULT_ENDPOINT
|
|
34
|
+
|
|
35
|
+
if not self.api_key:
|
|
36
|
+
raise ValueError("缺少配置,请设置环境变量: ARK_API_KEY")
|
|
37
|
+
|
|
38
|
+
def chat(self, model: str, content: str) -> ChatResult:
|
|
39
|
+
"""进行一轮对话
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
model: 模型名称
|
|
43
|
+
content: 用户输入内容
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
ChatResult 包含回复内容、耗时和 token 使用量
|
|
47
|
+
"""
|
|
48
|
+
data = {
|
|
49
|
+
"model": model,
|
|
50
|
+
"messages": [
|
|
51
|
+
{
|
|
52
|
+
"role": "user",
|
|
53
|
+
"content": [
|
|
54
|
+
{
|
|
55
|
+
"type": "text",
|
|
56
|
+
"text": content,
|
|
57
|
+
}
|
|
58
|
+
],
|
|
59
|
+
}
|
|
60
|
+
],
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
headers = {
|
|
64
|
+
"Content-Type": "application/json",
|
|
65
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
req = urllib.request.Request(
|
|
69
|
+
self.endpoint,
|
|
70
|
+
data=json.dumps(data).encode("utf-8"),
|
|
71
|
+
headers=headers,
|
|
72
|
+
method="POST",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
try:
|
|
76
|
+
start_time = time.time()
|
|
77
|
+
with urllib.request.urlopen(req) as response:
|
|
78
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
79
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
80
|
+
|
|
81
|
+
usage = result.get("usage", {})
|
|
82
|
+
return ChatResult(
|
|
83
|
+
content=result["choices"][0]["message"]["content"],
|
|
84
|
+
duration_ms=duration_ms,
|
|
85
|
+
input_tokens=usage.get("prompt_tokens", 0),
|
|
86
|
+
output_tokens=usage.get("completion_tokens", 0),
|
|
87
|
+
)
|
|
88
|
+
except urllib.error.HTTPError as e:
|
|
89
|
+
error_body = e.read().decode("utf-8")
|
|
90
|
+
raise RuntimeError(f"API 调用失败 ({e.code}): {error_body}")
|
ark_cli/search_client.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""联网搜索客户端"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
import time
|
|
8
|
+
import urllib.request
|
|
9
|
+
import urllib.error
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass
|
|
14
|
+
class SearchResult:
|
|
15
|
+
"""搜索结果项"""
|
|
16
|
+
|
|
17
|
+
title: str
|
|
18
|
+
site_name: str
|
|
19
|
+
url: str
|
|
20
|
+
snippet: str
|
|
21
|
+
publish_time: str
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass
|
|
25
|
+
class SearchResponse:
|
|
26
|
+
"""搜索响应"""
|
|
27
|
+
|
|
28
|
+
results: list[SearchResult]
|
|
29
|
+
result_count: int
|
|
30
|
+
duration_ms: int
|
|
31
|
+
query: str
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class SearchClient:
|
|
35
|
+
"""火山引擎联网搜索客户端"""
|
|
36
|
+
|
|
37
|
+
DEFAULT_ENDPOINT = "https://open.feedcoopapi.com/search_api/web_search"
|
|
38
|
+
|
|
39
|
+
def __init__(self, api_key: str | None = None, endpoint: str | None = None):
|
|
40
|
+
self.api_key = api_key or os.environ.get("SEARCH_API_KEY")
|
|
41
|
+
self.endpoint = endpoint or os.environ.get("SEARCH_ENDPOINT") or self.DEFAULT_ENDPOINT
|
|
42
|
+
|
|
43
|
+
if not self.api_key:
|
|
44
|
+
raise ValueError("缺少配置,请设置环境变量: SEARCH_API_KEY")
|
|
45
|
+
|
|
46
|
+
def search(
|
|
47
|
+
self,
|
|
48
|
+
query: str,
|
|
49
|
+
search_type: str = "web",
|
|
50
|
+
count: int = 10,
|
|
51
|
+
time_range: str = "OneWeek",
|
|
52
|
+
) -> SearchResponse:
|
|
53
|
+
"""执行搜索
|
|
54
|
+
|
|
55
|
+
Args:
|
|
56
|
+
query: 搜索关键词 (1-100字符)
|
|
57
|
+
search_type: 搜索类型 (web/web_summary/image)
|
|
58
|
+
count: 返回条数 (最多50条,默认10条)
|
|
59
|
+
time_range: 时间范围 (OneDay/OneWeek/OneMonth/OneYear)
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
SearchResponse 包含搜索结果列表和统计信息
|
|
63
|
+
"""
|
|
64
|
+
data: dict = {
|
|
65
|
+
"Query": query,
|
|
66
|
+
"SearchType": search_type,
|
|
67
|
+
"Count": count,
|
|
68
|
+
"Filter": {
|
|
69
|
+
"NeedUrl": True,
|
|
70
|
+
"AuthInfoLevel": 1,
|
|
71
|
+
"NeedContent": True
|
|
72
|
+
},
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
data["TimeRange"] = time_range
|
|
76
|
+
|
|
77
|
+
headers = {
|
|
78
|
+
"Content-Type": "application/json",
|
|
79
|
+
"Authorization": f"Bearer {self.api_key}",
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
req = urllib.request.Request(
|
|
83
|
+
self.endpoint,
|
|
84
|
+
data=json.dumps(data).encode("utf-8"),
|
|
85
|
+
headers=headers,
|
|
86
|
+
method="POST",
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
try:
|
|
90
|
+
start_time = time.time()
|
|
91
|
+
with urllib.request.urlopen(req) as response:
|
|
92
|
+
result = json.loads(response.read().decode("utf-8"))
|
|
93
|
+
duration_ms = int((time.time() - start_time) * 1000)
|
|
94
|
+
|
|
95
|
+
# 检查错误
|
|
96
|
+
if "ResponseMetadata" in result:
|
|
97
|
+
metadata = result["ResponseMetadata"]
|
|
98
|
+
if "Error" in metadata:
|
|
99
|
+
error = metadata["Error"]
|
|
100
|
+
raise RuntimeError(f"API 错误 ({error.get('CodeN', 'unknown')}): {error.get('Message', 'unknown error')}")
|
|
101
|
+
|
|
102
|
+
# 解析结果
|
|
103
|
+
api_result = result.get("Result", {})
|
|
104
|
+
web_results = api_result.get("WebResults", []) or []
|
|
105
|
+
search_context = api_result.get("SearchContext", {})
|
|
106
|
+
|
|
107
|
+
results = []
|
|
108
|
+
for item in web_results:
|
|
109
|
+
results.append(SearchResult(
|
|
110
|
+
title=item.get("Title", ""),
|
|
111
|
+
site_name=item.get("SiteName", ""),
|
|
112
|
+
url=item.get("Url", ""),
|
|
113
|
+
snippet=item.get("Snippet", ""),
|
|
114
|
+
publish_time=item.get("PublishTime", ""),
|
|
115
|
+
))
|
|
116
|
+
|
|
117
|
+
return SearchResponse(
|
|
118
|
+
results=results,
|
|
119
|
+
result_count=api_result.get("ResultCount", 0),
|
|
120
|
+
duration_ms=duration_ms,
|
|
121
|
+
query=search_context.get("OriginQuery", query),
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
except urllib.error.HTTPError as e:
|
|
125
|
+
error_body = e.read().decode("utf-8")
|
|
126
|
+
raise RuntimeError(f"API 调用失败 ({e.code}): {error_body}")
|
ark_cli/tos_client.py
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"""TOS 对象存储客户端"""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
|
|
7
|
+
import tos
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TosClient:
|
|
11
|
+
"""火山引擎 TOS 对象存储客户端"""
|
|
12
|
+
|
|
13
|
+
DEFAULT_ENDPOINT = "tos-cn-beijing.volces.com"
|
|
14
|
+
DEFAULT_REGION = "cn-beijing"
|
|
15
|
+
|
|
16
|
+
def __init__(
|
|
17
|
+
self,
|
|
18
|
+
ak: str | None = None,
|
|
19
|
+
sk: str | None = None,
|
|
20
|
+
endpoint: str | None = None,
|
|
21
|
+
region: str | None = None,
|
|
22
|
+
):
|
|
23
|
+
self.ak = ak or os.environ.get("TOS_ACCESS_KEY")
|
|
24
|
+
self.sk = sk or os.environ.get("TOS_SECRET_KEY")
|
|
25
|
+
self.endpoint = endpoint or os.environ.get("TOS_ENDPOINT") or self.DEFAULT_ENDPOINT
|
|
26
|
+
self.region = region or os.environ.get("TOS_REGION") or self.DEFAULT_REGION
|
|
27
|
+
|
|
28
|
+
if not all([self.ak, self.sk]):
|
|
29
|
+
raise ValueError(
|
|
30
|
+
"缺少 TOS 配置,请设置环境变量: TOS_ACCESS_KEY, TOS_SECRET_KEY"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
self.client = tos.TosClientV2(
|
|
34
|
+
ak=self.ak,
|
|
35
|
+
sk=self.sk,
|
|
36
|
+
endpoint=self.endpoint,
|
|
37
|
+
region=self.region,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
def upload(self, bucket: str, local_path: str, object_key: str, expires: int = 86400) -> str:
|
|
41
|
+
"""上传文件到 TOS
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
bucket: bucket 名称
|
|
45
|
+
local_path: 本地文件路径
|
|
46
|
+
object_key: 云上对象 key
|
|
47
|
+
expires: 临时链接过期时间(秒),默认 24 小时
|
|
48
|
+
|
|
49
|
+
Returns:
|
|
50
|
+
带签名的临时访问链接
|
|
51
|
+
"""
|
|
52
|
+
if not os.path.exists(local_path):
|
|
53
|
+
raise FileNotFoundError(f"文件不存在: {local_path}")
|
|
54
|
+
|
|
55
|
+
self.client.put_object_from_file(bucket, object_key, local_path)
|
|
56
|
+
|
|
57
|
+
# 生成预签名临时链接
|
|
58
|
+
result = self.client.pre_signed_url(
|
|
59
|
+
http_method=tos.HttpMethodType.Http_Method_Get,
|
|
60
|
+
bucket=bucket,
|
|
61
|
+
key=object_key,
|
|
62
|
+
expires=expires,
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
return result.signed_url
|
|
66
|
+
|
|
67
|
+
def list_objects(self, bucket: str, prefix: str = "", max_keys: int = 1000) -> list[dict]:
|
|
68
|
+
"""列出指定路径下的文件
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
bucket: bucket 名称
|
|
72
|
+
prefix: 路径前缀
|
|
73
|
+
max_keys: 最大返回数量
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
文件列表,每个元素包含 key, size, last_modified
|
|
77
|
+
"""
|
|
78
|
+
result = self.client.list_objects(
|
|
79
|
+
bucket,
|
|
80
|
+
prefix=prefix,
|
|
81
|
+
max_keys=max_keys,
|
|
82
|
+
delimiter="/",
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
items = []
|
|
86
|
+
|
|
87
|
+
# 添加目录(common_prefixes)
|
|
88
|
+
for cp in result.common_prefixes:
|
|
89
|
+
items.append({
|
|
90
|
+
"key": cp.prefix,
|
|
91
|
+
"size": "-",
|
|
92
|
+
"last_modified": "-",
|
|
93
|
+
"type": "dir",
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
# 添加文件
|
|
97
|
+
for obj in result.contents:
|
|
98
|
+
# 跳过与 prefix 完全相同的项(目录本身)
|
|
99
|
+
if obj.key == prefix:
|
|
100
|
+
continue
|
|
101
|
+
items.append({
|
|
102
|
+
"key": obj.key,
|
|
103
|
+
"size": obj.size,
|
|
104
|
+
"last_modified": obj.last_modified.strftime("%Y-%m-%d %H:%M:%S"),
|
|
105
|
+
"type": "file",
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
return items
|
|
109
|
+
|
|
110
|
+
def get_url(self, bucket: str, object_key: str, expires: int = 86400) -> str:
|
|
111
|
+
"""获取对象的临时访问链接
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
bucket: bucket 名称
|
|
115
|
+
object_key: 云上对象 key
|
|
116
|
+
expires: 临时链接过期时间(秒),默认 24 小时
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
带签名的临时访问链接
|
|
120
|
+
"""
|
|
121
|
+
result = self.client.pre_signed_url(
|
|
122
|
+
http_method=tos.HttpMethodType.Http_Method_Get,
|
|
123
|
+
bucket=bucket,
|
|
124
|
+
key=object_key,
|
|
125
|
+
expires=expires,
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
return result.signed_url
|
|
129
|
+
|
|
130
|
+
def list_buckets(self) -> list[dict]:
|
|
131
|
+
"""列出所有 bucket
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
bucket 列表,每个元素包含 name, creation_date
|
|
135
|
+
"""
|
|
136
|
+
result = self.client.list_buckets()
|
|
137
|
+
|
|
138
|
+
items = []
|
|
139
|
+
for bucket in result.buckets:
|
|
140
|
+
creation_date = bucket.creation_date
|
|
141
|
+
# 兼容 datetime 对象和字符串
|
|
142
|
+
if hasattr(creation_date, "strftime"):
|
|
143
|
+
creation_date = creation_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
144
|
+
items.append({
|
|
145
|
+
"name": bucket.name,
|
|
146
|
+
"creation_date": creation_date,
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
return items
|
|
150
|
+
|
|
151
|
+
def delete(self, bucket: str, object_key: str) -> None:
|
|
152
|
+
"""删除对象
|
|
153
|
+
|
|
154
|
+
Args:
|
|
155
|
+
bucket: bucket 名称
|
|
156
|
+
object_key: 云上对象 key
|
|
157
|
+
"""
|
|
158
|
+
self.client.delete_object(bucket, object_key)
|
|
159
|
+
|
|
160
|
+
def stats(self, bucket: str) -> dict:
|
|
161
|
+
"""获取 bucket 元数据
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
bucket: bucket 名称
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
包含 bucket 元数据的字典
|
|
168
|
+
"""
|
|
169
|
+
result = self.client.get_bucket_info(bucket)
|
|
170
|
+
info = result.bucket_info
|
|
171
|
+
|
|
172
|
+
creation_date = info.creation_date
|
|
173
|
+
if hasattr(creation_date, "strftime"):
|
|
174
|
+
creation_date = creation_date.strftime("%Y-%m-%d %H:%M:%S")
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
"name": info.name,
|
|
178
|
+
"location": info.location,
|
|
179
|
+
"creation_date": creation_date,
|
|
180
|
+
"storage_class": str(info.storage_class).replace("StorageClassType.", ""),
|
|
181
|
+
"az_redundancy": str(info.az_redundancy).replace("AzRedundancyType.", ""),
|
|
182
|
+
"versioning": str(info.versioning).replace("VersioningStatusType.", ""),
|
|
183
|
+
"extranet_endpoint": info.extranet_endpoint,
|
|
184
|
+
"intranet_endpoint": info.intranet_endpoint,
|
|
185
|
+
}
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ark-utils-cli
|
|
3
|
+
Version: 0.3.0
|
|
4
|
+
Summary: 火山引擎命令行工具 - 让人类和 AI Agent 都能在终端中操作火山引擎
|
|
5
|
+
License-File: LICENSE
|
|
6
|
+
Requires-Python: >=3.9
|
|
7
|
+
Requires-Dist: tos>=2.9.0
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
|
|
10
|
+
<p align="center">
|
|
11
|
+
<h1 align="center">ark-cli</h1>
|
|
12
|
+
</p>
|
|
13
|
+
|
|
14
|
+
<p align="center">
|
|
15
|
+
火山引擎命令行工具 — 让人类和 AI Agent 都能在终端中操作火山引擎
|
|
16
|
+
</p>
|
|
17
|
+
|
|
18
|
+
<p align="center">
|
|
19
|
+
<a href="https://github.com/cnwarden/ark-cli/releases"><img src="https://img.shields.io/github/v/release/cnwarden/ark-cli?style=for-the-badge&color=FF6A00" alt="Release" /></a>
|
|
20
|
+
<a href="https://www.python.org/"><img src="https://img.shields.io/badge/Python-3.9+-3776AB?style=for-the-badge&logo=python&logoColor=white" alt="Python" /></a>
|
|
21
|
+
<a href="https://github.com/cnwarden/ark-cli/stargazers"><img src="https://img.shields.io/github/stars/cnwarden/ark-cli?style=for-the-badge&color=f5a623" alt="Stars" /></a>
|
|
22
|
+
<a href="https://github.com/cnwarden/ark-cli/blob/main/LICENSE"><img src="https://img.shields.io/badge/License-MIT-teal.svg?style=for-the-badge" alt="License" /></a>
|
|
23
|
+
</p>
|
|
24
|
+
|
|
25
|
+
<p align="center">
|
|
26
|
+
<a href="#ark-cli-是什么">介绍</a> · <a href="#核心能力">核心能力</a> · <a href="#快速开始">快速开始</a> · <a href="#命令参考">命令参考</a> · <a href="#贡献">贡献</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## ark-cli 是什么
|
|
32
|
+
|
|
33
|
+
ark-cli 是火山引擎的命令行工具,将 TOS 对象存储、大模型服务、联网搜索等操作封装为简洁的命令行接口。
|
|
34
|
+
|
|
35
|
+
### 为什么选择 ark-cli
|
|
36
|
+
|
|
37
|
+
- **开箱即用** — 默认配置北京区域,最少只需 2 个环境变量即可开始使用
|
|
38
|
+
- **AI Agent 友好** — 命令设计简洁,输出格式规范,便于 AI Agent 解析调用
|
|
39
|
+
- **临时链接** — 上传文件自动返回 24 小时有效的签名 URL
|
|
40
|
+
- **轻量依赖** — 基于 Python,使用 uv 包管理,安装简单
|
|
41
|
+
|
|
42
|
+
## 核心能力
|
|
43
|
+
|
|
44
|
+
| 模块 | 能力 |
|
|
45
|
+
|------|------|
|
|
46
|
+
| **TOS 对象存储** | 文件上传、列表、删除、临时链接生成、Bucket 统计 |
|
|
47
|
+
| **大模型服务** | 单轮对话、Token 用量统计、耗时统计 |
|
|
48
|
+
| **联网搜索** | 网页搜索、结果摘要、时间范围过滤 |
|
|
49
|
+
|
|
50
|
+
## 快速开始
|
|
51
|
+
|
|
52
|
+
### 安装
|
|
53
|
+
|
|
54
|
+
**从源码安装**
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/cnwarden/ark-cli.git
|
|
58
|
+
cd ark-cli
|
|
59
|
+
uv sync
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
**从 Release 下载**
|
|
63
|
+
|
|
64
|
+
从 [Releases](https://github.com/cnwarden/ark-cli/releases/latest) 页面下载最新的 wheel 包:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install ark_cli-*.whl
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 配置凭证
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
# TOS 对象存储(必需)
|
|
74
|
+
export TOS_ACCESS_KEY="your-access-key"
|
|
75
|
+
export TOS_SECRET_KEY="your-secret-key"
|
|
76
|
+
|
|
77
|
+
# TOS 可选配置(默认北京区域)
|
|
78
|
+
export TOS_ENDPOINT="tos-cn-beijing.volces.com"
|
|
79
|
+
export TOS_REGION="cn-beijing"
|
|
80
|
+
|
|
81
|
+
# 大模型服务(使用 model 功能时必需)
|
|
82
|
+
export ARK_API_KEY="your-api-key"
|
|
83
|
+
|
|
84
|
+
# 联网搜索(使用 search 功能时必需)
|
|
85
|
+
export SEARCH_API_KEY="your-search-api-key"
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### 验证安装
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
ark-cli -v
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## 命令参考
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
ark-cli [-p PRODUCT] [-a ACTION] [OPTIONS]
|
|
98
|
+
|
|
99
|
+
Products:
|
|
100
|
+
tos TOS 对象存储操作
|
|
101
|
+
model 大模型对话
|
|
102
|
+
search 联网搜索
|
|
103
|
+
|
|
104
|
+
TOS Actions:
|
|
105
|
+
upload 上传文件
|
|
106
|
+
ls 列出文件
|
|
107
|
+
lsb 列出 Bucket
|
|
108
|
+
geturl 获取临时链接
|
|
109
|
+
rm 删除文件
|
|
110
|
+
stats Bucket 统计信息
|
|
111
|
+
|
|
112
|
+
Model Options:
|
|
113
|
+
-m 模型名称(例如: doubao-seed-2-0-mini-260215)
|
|
114
|
+
-c 对话内容
|
|
115
|
+
|
|
116
|
+
Search Options:
|
|
117
|
+
-q 搜索关键词
|
|
118
|
+
--count 返回条数(默认: 10, 最多: 50)
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
<details>
|
|
122
|
+
<summary>TOS 对象存储</summary>
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
# 上传文件(返回 24 小时有效的签名链接)
|
|
126
|
+
ark-cli -p tos -a upload -bucket my-bucket -i ./test.txt -o data/test.txt
|
|
127
|
+
|
|
128
|
+
# 列出 Bucket
|
|
129
|
+
ark-cli -p tos -a lsb
|
|
130
|
+
|
|
131
|
+
# 列出文件
|
|
132
|
+
ark-cli -p tos -a ls -bucket my-bucket
|
|
133
|
+
ark-cli -p tos -a ls -bucket my-bucket -i data/
|
|
134
|
+
|
|
135
|
+
# 获取临时链接
|
|
136
|
+
ark-cli -p tos -a geturl -bucket my-bucket -i data/test.txt
|
|
137
|
+
|
|
138
|
+
# 删除文件
|
|
139
|
+
ark-cli -p tos -a rm -bucket my-bucket -i data/test.txt
|
|
140
|
+
|
|
141
|
+
# Bucket 统计信息
|
|
142
|
+
ark-cli -p tos -a stats -bucket my-bucket
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
**输出示例**
|
|
146
|
+
|
|
147
|
+
列出文件:
|
|
148
|
+
```
|
|
149
|
+
DIR - data/subdir/
|
|
150
|
+
1.2KB 2024-03-30 10:00:00 data/test.txt
|
|
151
|
+
256B 2024-03-30 09:30:00 data/config.json
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Bucket 统计:
|
|
155
|
+
```
|
|
156
|
+
Bucket: my-bucket
|
|
157
|
+
Location: cn-beijing
|
|
158
|
+
Created: 2025-05-12 12:29:06
|
|
159
|
+
Storage: Storage_Class_Standard
|
|
160
|
+
Redundancy: Az_Redundancy_Single_Az
|
|
161
|
+
Versioning: Versioning_Unknown
|
|
162
|
+
Endpoint: tos-cn-beijing.volces.com
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
</details>
|
|
166
|
+
|
|
167
|
+
<details>
|
|
168
|
+
<summary>大模型对话</summary>
|
|
169
|
+
|
|
170
|
+
```bash
|
|
171
|
+
# 单轮对话
|
|
172
|
+
ark-cli -p model -m doubao-seed-2-0-mini-260215 -c "你好,请介绍一下你自己"
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
**输出示例**
|
|
176
|
+
```
|
|
177
|
+
嗨!👋 你好呀~ 请问有什么我可以帮你的吗?
|
|
178
|
+
|
|
179
|
+
[耗时: 5991ms | tokens: 50 in / 371 out]
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
</details>
|
|
183
|
+
|
|
184
|
+
<details>
|
|
185
|
+
<summary>联网搜索</summary>
|
|
186
|
+
|
|
187
|
+
```bash
|
|
188
|
+
# 基础搜索
|
|
189
|
+
ark-cli -p search -q "火山引擎"
|
|
190
|
+
|
|
191
|
+
# 指定返回条数
|
|
192
|
+
ark-cli -p search -q "Python 教程" --count 5
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
**输出示例**
|
|
196
|
+
```
|
|
197
|
+
[1] 火山引擎官网 - 字节跳动旗下云服务平台
|
|
198
|
+
火山引擎 | 2025-03-30
|
|
199
|
+
https://www.volcengine.com/
|
|
200
|
+
火山引擎是字节跳动旗下的云服务平台,提供云计算、大数据、人工智能等服务...
|
|
201
|
+
|
|
202
|
+
[2] 火山引擎开放平台 - 开发者文档
|
|
203
|
+
火山引擎 | 2025-03-28
|
|
204
|
+
https://www.volcengine.com/docs
|
|
205
|
+
火山引擎开放平台提供丰富的 API 和 SDK,帮助开发者快速接入...
|
|
206
|
+
|
|
207
|
+
[共 10 条结果 | 耗时: 372ms]
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
</details>
|
|
211
|
+
|
|
212
|
+
## 技术栈
|
|
213
|
+
|
|
214
|
+
| 组件 | 选型 | 说明 |
|
|
215
|
+
|------|------|------|
|
|
216
|
+
| 语言 | [Python](https://www.python.org/) 3.9+ | |
|
|
217
|
+
| 包管理 | [uv](https://github.com/astral-sh/uv) | 快速的 Python 包管理器 |
|
|
218
|
+
| TOS SDK | [tos](https://github.com/volcengine/ve-tos-python-sdk) | 火山引擎 TOS 官方 SDK |
|
|
219
|
+
| 构建工具 | [hatchling](https://hatch.pypa.io/) | Python 构建后端 |
|
|
220
|
+
|
|
221
|
+
## 项目结构
|
|
222
|
+
|
|
223
|
+
```
|
|
224
|
+
ark-cli/
|
|
225
|
+
├── ark_cli/
|
|
226
|
+
│ ├── __init__.py # 版本定义
|
|
227
|
+
│ ├── cli.py # CLI 入口
|
|
228
|
+
│ ├── tos_client.py # TOS 客户端封装
|
|
229
|
+
│ ├── model_client.py # 大模型客户端
|
|
230
|
+
│ └── search_client.py # 联网搜索客户端
|
|
231
|
+
├── pyproject.toml # 项目配置
|
|
232
|
+
├── Makefile # 构建脚本
|
|
233
|
+
└── README.md
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
## 开发
|
|
237
|
+
|
|
238
|
+
```bash
|
|
239
|
+
# 克隆项目
|
|
240
|
+
git clone https://github.com/cnwarden/ark-cli.git
|
|
241
|
+
cd ark-cli
|
|
242
|
+
|
|
243
|
+
# 安装依赖
|
|
244
|
+
uv sync
|
|
245
|
+
|
|
246
|
+
# 运行
|
|
247
|
+
uv run ark-cli -v
|
|
248
|
+
|
|
249
|
+
# 构建
|
|
250
|
+
uv build
|
|
251
|
+
|
|
252
|
+
# 发布到 TOS
|
|
253
|
+
make publish
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## 贡献
|
|
257
|
+
|
|
258
|
+
欢迎提交 Issue 和 Pull Request!
|
|
259
|
+
|
|
260
|
+
1. Fork 本仓库
|
|
261
|
+
2. 创建特性分支:`git checkout -b feature/amazing-feature`
|
|
262
|
+
3. 提交更改:`git commit -m 'feat: add amazing feature'`
|
|
263
|
+
4. 推送分支:`git push origin feature/amazing-feature`
|
|
264
|
+
5. 提交 Pull Request
|
|
265
|
+
|
|
266
|
+
提交信息请遵循 [Conventional Commits](https://www.conventionalcommits.org/) 规范。
|
|
267
|
+
|
|
268
|
+
## License
|
|
269
|
+
|
|
270
|
+
[MIT](LICENSE)
|
|
271
|
+
|
|
272
|
+
## 相关链接
|
|
273
|
+
|
|
274
|
+
- [火山引擎控制台](https://console.volcengine.com/) — 获取 Access Key
|
|
275
|
+
- [TOS 文档](https://www.volcengine.com/docs/6349) — 对象存储文档
|
|
276
|
+
- [方舟大模型](https://www.volcengine.com/docs/82379) — 大模型服务文档
|
|
277
|
+
- [联网搜索 API](https://www.volcengine.com/docs/87772/2272953) — 联网搜索文档
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
ark_cli/__init__.py,sha256=6_kreJnX_xCuH6ty5hsJsg-F9qVjFq8xlYradAAfV-Q,57
|
|
2
|
+
ark_cli/cli.py,sha256=xGkp4w06N66b8LLwzXcn2dNPSa4szwIhXMUYd6Kf660,8124
|
|
3
|
+
ark_cli/model_client.py,sha256=77EQLnYbFMazcZRG8TwGSNgRDeYORyebU3r38ipaF4I,2563
|
|
4
|
+
ark_cli/search_client.py,sha256=J8gncQMCG_16AmVvWvLCd7WFNV7OqN489Mw7AAhUhAM,3759
|
|
5
|
+
ark_cli/tos_client.py,sha256=VxhM_oqTCqZ7KqC7eEvL2qjhK1TtImsQsu41PJhi4Rc,5606
|
|
6
|
+
ark_utils_cli-0.3.0.dist-info/METADATA,sha256=kMbbgXD2mVqPInGOG2ZiS5rDG7aITUmwRbWTlsb3EtQ,7355
|
|
7
|
+
ark_utils_cli-0.3.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
8
|
+
ark_utils_cli-0.3.0.dist-info/entry_points.txt,sha256=s-b0LrOd_0hzcpUfTy8ungGP0QLOhL75R6sAzpLfcyg,45
|
|
9
|
+
ark_utils_cli-0.3.0.dist-info/licenses/LICENSE,sha256=hpECGeLwcNtHJ51vjSm_gU6DL7LfCLnpUkA5n8MDIXs,1065
|
|
10
|
+
ark_utils_cli-0.3.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 cnwarden
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|