adam-community 1.0.25__tar.gz → 1.0.26__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 (39) hide show
  1. {adam_community-1.0.25 → adam_community-1.0.26}/PKG-INFO +19 -1
  2. {adam_community-1.0.25 → adam_community-1.0.26}/README.md +18 -0
  3. adam_community-1.0.26/adam_community/__version__.py +1 -0
  4. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/cli.py +4 -0
  5. adam_community-1.0.26/adam_community/cli/sif_build.py +576 -0
  6. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community.egg-info/PKG-INFO +19 -1
  7. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community.egg-info/SOURCES.txt +2 -0
  8. adam_community-1.0.26/test/test_sif_upload.py +444 -0
  9. adam_community-1.0.25/adam_community/__version__.py +0 -1
  10. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/__init__.py +0 -0
  11. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/__init__.py +0 -0
  12. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/build.py +0 -0
  13. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/init.py +0 -0
  14. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/parser.py +0 -0
  15. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/Makefile.j2 +0 -0
  16. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/README_agent.md.j2 +0 -0
  17. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/README_kit.md.j2 +0 -0
  18. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/__init__.py +0 -0
  19. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/agent_python.py.j2 +0 -0
  20. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/configure.json.j2 +0 -0
  21. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/initial_assistant_message.md.j2 +0 -0
  22. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/initial_assistant_message_en.md.j2 +0 -0
  23. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/initial_system_prompt.md.j2 +0 -0
  24. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/initial_system_prompt_en.md.j2 +0 -0
  25. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/input.json.j2 +0 -0
  26. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/kit_python.py.j2 +0 -0
  27. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/long_description.md.j2 +0 -0
  28. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/long_description_en.md.j2 +0 -0
  29. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/templates/rag_python.py.j2 +0 -0
  30. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/cli/updater.py +0 -0
  31. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/tool.py +0 -0
  32. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community/util.py +0 -0
  33. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community.egg-info/dependency_links.txt +0 -0
  34. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community.egg-info/entry_points.txt +0 -0
  35. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community.egg-info/requires.txt +0 -0
  36. {adam_community-1.0.25 → adam_community-1.0.26}/adam_community.egg-info/top_level.txt +0 -0
  37. {adam_community-1.0.25 → adam_community-1.0.26}/setup.cfg +0 -0
  38. {adam_community-1.0.25 → adam_community-1.0.26}/setup.py +0 -0
  39. {adam_community-1.0.25 → adam_community-1.0.26}/test/test_util_tool.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: adam_community
3
- Version: 1.0.25
3
+ Version: 1.0.26
4
4
  Summary: Adam Community Tools and Utilities
5
5
  Home-page: https://github.com/yourusername/adam-community
6
6
  Author: Adam Community
@@ -67,6 +67,23 @@ adam-cli build .
67
67
  adam-cli update
68
68
  ```
69
69
 
70
+ #### SIF 文件管理
71
+
72
+ 管理 SIF 文件,包括上传到 Docker 镜像仓库等操作:
73
+
74
+ ```bash
75
+ # 查看帮助
76
+ adam-cli sif --help
77
+
78
+ # 上传 SIF 文件到镜像仓库(自适应切片)
79
+ adam-cli sif upload ./xxx.sif registry.example.com/image:1.0.0
80
+
81
+ # 带认证上传
82
+ adam-cli sif upload ./app.sif registry.cn-hangzhou.aliyuncs.com/ns/app:latest \
83
+ --username user --password pass
84
+
85
+ ```
86
+
70
87
  ### Python 模块导入
71
88
 
72
89
  ```python
@@ -116,6 +133,7 @@ trackPath("/path/to/cache")
116
133
  - **项目构建**: 检查配置文件、文档文件并创建 zip 包
117
134
  - **类型检查**: 支持多种 Python 类型注解格式
118
135
  - **自动更新**: 智能检查和更新到最新版本,支持用户配置
136
+ - **SIF 镜像构建**: 将 SIF 文件切片并构建 Docker 镜像推送到仓库
119
137
 
120
138
  ## 开发
121
139
 
@@ -37,6 +37,23 @@ adam-cli build .
37
37
  adam-cli update
38
38
  ```
39
39
 
40
+ #### SIF 文件管理
41
+
42
+ 管理 SIF 文件,包括上传到 Docker 镜像仓库等操作:
43
+
44
+ ```bash
45
+ # 查看帮助
46
+ adam-cli sif --help
47
+
48
+ # 上传 SIF 文件到镜像仓库(自适应切片)
49
+ adam-cli sif upload ./xxx.sif registry.example.com/image:1.0.0
50
+
51
+ # 带认证上传
52
+ adam-cli sif upload ./app.sif registry.cn-hangzhou.aliyuncs.com/ns/app:latest \
53
+ --username user --password pass
54
+
55
+ ```
56
+
40
57
  ### Python 模块导入
41
58
 
42
59
  ```python
@@ -86,6 +103,7 @@ trackPath("/path/to/cache")
86
103
  - **项目构建**: 检查配置文件、文档文件并创建 zip 包
87
104
  - **类型检查**: 支持多种 Python 类型注解格式
88
105
  - **自动更新**: 智能检查和更新到最新版本,支持用户配置
106
+ - **SIF 镜像构建**: 将 SIF 文件切片并构建 Docker 镜像推送到仓库
89
107
 
90
108
  ## 开发
91
109
 
@@ -0,0 +1 @@
1
+ __version__ = "1.0.26"
@@ -5,6 +5,7 @@ from .parser import parse_directory
5
5
  from .build import build_package
6
6
  from .init import init
7
7
  from .updater import check_and_notify_update, update_cli, set_update_disabled
8
+ from .sif_build import sif
8
9
  from ..__version__ import __version__
9
10
 
10
11
  @click.group()
@@ -76,5 +77,8 @@ def config(disable_update_check, enable_update_check):
76
77
  # 添加 init 命令
77
78
  cli.add_command(init)
78
79
 
80
+ # 添加 sif 命令组
81
+ cli.add_command(sif)
82
+
79
83
  if __name__ == '__main__':
80
84
  cli()
@@ -0,0 +1,576 @@
1
+ import click
2
+ import os
3
+ import subprocess
4
+ import sys
5
+ import re
6
+ import shutil
7
+ from pathlib import Path
8
+ from typing import Tuple, List, Optional
9
+ from rich.console import Console
10
+ from rich.panel import Panel
11
+
12
+ console = Console()
13
+
14
+
15
+ def validateSifFile(sif_path: Path) -> Tuple[bool, str]:
16
+ """验证 SIF 文件是否有效
17
+
18
+ Args:
19
+ sif_path: SIF 文件路径
20
+
21
+ Returns:
22
+ Tuple[bool, str]: (是否有效, 错误消息)
23
+ """
24
+ if not sif_path.exists():
25
+ return False, f"SIF 文件不存在: {sif_path}"
26
+
27
+ if not sif_path.is_file():
28
+ return False, f"路径不是文件: {sif_path}"
29
+
30
+ if not os.access(sif_path, os.R_OK):
31
+ return False, f"SIF 文件不可读: {sif_path}"
32
+
33
+ file_size = sif_path.stat().st_size
34
+ if file_size == 0:
35
+ return False, f"SIF 文件为空: {sif_path}"
36
+
37
+ return True, ""
38
+
39
+
40
+ def validateImageUrl(image_url: str) -> Tuple[bool, str]:
41
+ """验证 Docker 镜像 URL 格式
42
+
43
+ Args:
44
+ image_url: Docker 镜像 URL
45
+
46
+ Returns:
47
+ Tuple[bool, str]: (是否有效, 错误消息)
48
+ """
49
+ # 基本格式验证:registry/namespace/image:tag
50
+ # 支持域名和 IP 地址(带端口号)
51
+ pattern = r'^[a-zA-Z0-9\-\.]+(:[0-9]+)?(/[a-zA-Z0-9\-_]+)+:[a-zA-Z0-9\.\-_]+$'
52
+
53
+ if not re.match(pattern, image_url):
54
+ return False, "镜像 URL 格式不正确,应为 registry/namespace/image:tag"
55
+
56
+ return True, ""
57
+
58
+
59
+ def checkCommandAvailable(command: str) -> Tuple[bool, str, str]:
60
+ """检查命令是否可用
61
+
62
+ Args:
63
+ command: 命令名称
64
+
65
+ Returns:
66
+ Tuple[bool, str, str]: (是否可用, 安装提示, URL)
67
+ """
68
+ # 使用 which 命令检测命令是否存在(更可靠)
69
+ try:
70
+ result = subprocess.run(
71
+ ['which', command],
72
+ capture_output=True,
73
+ text=True,
74
+ timeout=5
75
+ )
76
+ if result.returncode == 0 and result.stdout.strip():
77
+ return True, "", ""
78
+ except (FileNotFoundError, subprocess.TimeoutExpired):
79
+ pass
80
+
81
+ # 如果 which 不可用,尝试直接运行命令
82
+ try:
83
+ # 对于 split,尝试运行一个简单的命令
84
+ if command == 'split':
85
+ # 使用 --help 而不是 --version(macOS 的 split 不支持 --version)
86
+ result = subprocess.run(
87
+ ['split', '--help'],
88
+ capture_output=True,
89
+ text=True,
90
+ timeout=5
91
+ )
92
+ if result.returncode == 0:
93
+ return True, "", ""
94
+ else:
95
+ # 其他命令尝试 --version
96
+ result = subprocess.run(
97
+ [command, '--version'],
98
+ capture_output=True,
99
+ text=True,
100
+ timeout=5
101
+ )
102
+ if result.returncode == 0:
103
+ return True, "", ""
104
+ except (FileNotFoundError, subprocess.TimeoutExpired):
105
+ pass
106
+
107
+ # 命令不可用,返回安装提示
108
+ install_hints = {
109
+ 'split': (
110
+ "split 命令未找到",
111
+ "split 是 macOS/Linux 系统自带命令\n\n"
112
+ "macOS: 已预装(如果提示缺少,请安装 Xcode Command Line Tools)\n"
113
+ " xcode-select --install\n\n"
114
+ "Linux: sudo apt-get install coreutils / sudo yum install coreutils"
115
+ ),
116
+ 'docker': (
117
+ "Docker 未安装或未运行",
118
+ "Docker 是容器化平台\n\n"
119
+ "macOS: 下载并安装 Docker Desktop\n"
120
+ " https://www.docker.com/products/docker-desktop/\n\n"
121
+ "Linux: 安装 Docker Engine\n"
122
+ " https://docs.docker.com/engine/install/\n\n"
123
+ "安装后请确保 Docker daemon 正在运行"
124
+ )
125
+ }
126
+
127
+ if command in install_hints:
128
+ hint, url = install_hints[command]
129
+ return False, hint, url
130
+
131
+ return False, f"{command} 命令未找到", ""
132
+
133
+
134
+ def checkDockerEnvironment() -> Tuple[bool, List[str]]:
135
+ """检查 Docker 是否可用
136
+
137
+ Returns:
138
+ Tuple[bool, List[str]]: (是否可用, 错误消息列表)
139
+ """
140
+ errors = []
141
+
142
+ # 检查 Docker 命令
143
+ docker_available, docker_hint, docker_url = checkCommandAvailable('docker')
144
+ if not docker_available:
145
+ errors.append(f"❌ {docker_hint}")
146
+ if docker_url:
147
+ errors.append(f"\n{docker_url}")
148
+ else:
149
+ # 检查 Docker daemon 是否运行
150
+ try:
151
+ result = subprocess.run(
152
+ ['docker', 'info'],
153
+ capture_output=True,
154
+ text=True,
155
+ timeout=5
156
+ )
157
+ if result.returncode != 0:
158
+ errors.append("\n⚠️ Docker daemon 未运行,请启动 Docker")
159
+ except Exception:
160
+ pass
161
+
162
+ return len(errors) == 0, errors
163
+
164
+
165
+ def checkRequiredCommands() -> Tuple[bool, List[str]]:
166
+ """检查所有必需的命令
167
+
168
+ Returns:
169
+ Tuple[bool, List[str]]: (是否全部可用, 错误消息列表)
170
+ """
171
+ all_errors = []
172
+
173
+ # 检查 split 命令
174
+ split_available, split_hint, split_url = checkCommandAvailable('split')
175
+ if not split_available:
176
+ error_msg = f"❌ {split_hint}"
177
+ if split_url:
178
+ error_msg += f"\n\n{split_url}"
179
+ all_errors.append(error_msg)
180
+
181
+ # 检查 Docker
182
+ docker_available, docker_errors = checkDockerEnvironment()
183
+ if not docker_available:
184
+ all_errors.extend(docker_errors)
185
+
186
+ return len(all_errors) == 0, all_errors
187
+
188
+
189
+ def createWorkDir(sif_path: Path) -> Path:
190
+ """创建临时工作目录
191
+
192
+ Args:
193
+ sif_path: SIF 文件路径
194
+
195
+ Returns:
196
+ Path: 工作目录路径
197
+ """
198
+ parent_dir = sif_path.parent
199
+ work_dir = parent_dir / ".sif_build_temp"
200
+ work_dir.mkdir(parents=True, exist_ok=True)
201
+ return work_dir
202
+
203
+
204
+ def calculateOptimalChunkSize(file_size_bytes: int) -> Optional[str]:
205
+ """根据文件大小自适应计算切片大小
206
+
207
+ Args:
208
+ file_size_bytes: 文件大小(字节)
209
+
210
+ Returns:
211
+ Optional[str]: 切片大小(如 '100M', '500M'),None 表示不切片
212
+ """
213
+ size_mb = file_size_bytes / (1024 * 1024)
214
+
215
+ if size_mb < 500:
216
+ return None # 不切片
217
+ elif size_mb < 2 * 1024:
218
+ return "100M"
219
+ elif size_mb < 10 * 1024:
220
+ return "500M"
221
+ else:
222
+ return "1G"
223
+
224
+
225
+ def splitSifFile(sif_path: Path, chunk_size: Optional[str], work_dir: Path) -> List[Path]:
226
+ """切片 SIF 文件
227
+
228
+ Args:
229
+ sif_path: SIF 文件路径
230
+ chunk_size: 切片大小(如 '100M'),None 表示不切片
231
+ work_dir: 工作目录
232
+
233
+ Returns:
234
+ List[Path]: 切片文件列表
235
+
236
+ Raises:
237
+ subprocess.CalledProcessError: split 命令执行失败
238
+ """
239
+ if chunk_size is None:
240
+ # 不切片,直接复制到工作目录
241
+ dest_file = work_dir / sif_path.name
242
+ shutil.copy2(sif_path, dest_file)
243
+ return [dest_file]
244
+
245
+ # 使用 split 命令切片
246
+ output_prefix = sif_path.name # 不包含路径
247
+ cmd = ['split', '-b', chunk_size, '-d', str(sif_path), output_prefix + '.']
248
+
249
+ result = subprocess.run(
250
+ cmd,
251
+ cwd=work_dir,
252
+ capture_output=True,
253
+ text=True,
254
+ check=True
255
+ )
256
+
257
+ # 查找所有切片文件
258
+ chunks = sorted(work_dir.glob(f"{output_prefix}.*"))
259
+ return chunks
260
+
261
+
262
+ def generateDockerfile(work_dir: Path) -> Path:
263
+ """生成 Dockerfile
264
+
265
+ Args:
266
+ work_dir: 工作目录
267
+
268
+ Returns:
269
+ Path: Dockerfile 文件路径
270
+ """
271
+ dockerfile_path = work_dir / "Dockerfile"
272
+
273
+ dockerfile_content = """FROM alpine
274
+ COPY . /sif
275
+ """
276
+
277
+ with open(dockerfile_path, 'w', encoding='utf-8') as f:
278
+ f.write(dockerfile_content)
279
+
280
+ return dockerfile_path
281
+
282
+
283
+ def executeCommand(cmd: List[str], description: str, console: Console) -> bool:
284
+ """执行命令并实时显示输出
285
+
286
+ Args:
287
+ cmd: 命令列表
288
+ description: 命令描述
289
+ console: Console 实例
290
+
291
+ Returns:
292
+ bool: 是否执行成功
293
+ """
294
+ console.print(f"\n[dim]执行命令: {' '.join(cmd)}[/dim]")
295
+
296
+ try:
297
+ process = subprocess.Popen(
298
+ cmd,
299
+ stdout=subprocess.PIPE,
300
+ stderr=subprocess.STDOUT,
301
+ universal_newlines=True,
302
+ bufsize=1
303
+ )
304
+
305
+ # 实时显示输出
306
+ for line in process.stdout:
307
+ console.print(line.rstrip())
308
+
309
+ process.wait()
310
+ return process.returncode == 0
311
+
312
+ except Exception as e:
313
+ console.print(f"[red]命令执行异常: {str(e)}[/red]")
314
+ return False
315
+
316
+
317
+ def buildDockerImage(work_dir: Path, image_url: str, console: Console) -> bool:
318
+ """构建 Docker 镜像
319
+
320
+ Args:
321
+ work_dir: 工作目录
322
+ image_url: 镜像 URL
323
+ console: Console 实例
324
+
325
+ Returns:
326
+ bool: 是否构建成功
327
+ """
328
+ # 指定架构为 x86_64,确保在不同平台上的兼容性
329
+ cmd = ['docker', 'build', '--platform', 'linux/amd64', '-t', image_url, str(work_dir)]
330
+ return executeCommand(cmd, "构建 Docker 镜像", console)
331
+
332
+
333
+ def pushDockerImage(image_url: str, username: Optional[str], password: Optional[str], console: Console) -> bool:
334
+ """推送 Docker 镜像
335
+
336
+ Args:
337
+ image_url: 镜像 URL
338
+ username: 用户名
339
+ password: 密码
340
+ console: Console 实例
341
+
342
+ Returns:
343
+ bool: 是否推送成功
344
+ """
345
+ # 如果提供了认证信息,先执行 docker login
346
+ if username and password:
347
+ console.print("\n[bold blue]🔐 登录 Docker 仓库[/bold blue]")
348
+
349
+ # 从镜像 URL 提取 registry
350
+ registry = image_url.split('/')[0]
351
+
352
+ # 使用 stdin 传递密码
353
+ cmd = ['docker', 'login', '-u', username, '--password-stdin', registry]
354
+
355
+ try:
356
+ process = subprocess.Popen(
357
+ cmd,
358
+ stdin=subprocess.PIPE,
359
+ stdout=subprocess.PIPE,
360
+ stderr=subprocess.PIPE,
361
+ universal_newlines=True
362
+ )
363
+
364
+ stdout, stderr = process.communicate(input=password)
365
+
366
+ if process.returncode != 0:
367
+ console.print(f"[red]登录失败: {stderr}[/red]")
368
+ return False
369
+
370
+ console.print("[green]✓ 登录成功[/green]")
371
+ except Exception as e:
372
+ console.print(f"[red]登录异常: {str(e)}[/red]")
373
+ return False
374
+
375
+ # 推送镜像
376
+ cmd = ['docker', 'push', image_url]
377
+ return executeCommand(cmd, "推送 Docker 镜像", console)
378
+
379
+
380
+ def cleanupTempFiles(work_dir: Path, keep_temp: bool, console: Console):
381
+ """清理临时文件
382
+
383
+ Args:
384
+ work_dir: 工作目录
385
+ keep_temp: 是否保留临时文件
386
+ console: Console 实例
387
+ """
388
+ if keep_temp:
389
+ console.print(f"\n[dim]临时文件保留在: {work_dir}[/dim]")
390
+ return
391
+
392
+ console.print(f"\n[bold blue]🧹 清理临时文件[/bold blue]")
393
+
394
+ try:
395
+ shutil.rmtree(work_dir)
396
+ console.print(f"[green]✓ 已清理: {work_dir}[/green]")
397
+ except Exception as e:
398
+ console.print(f"[yellow]⚠️ 清理失败: {str(e)}[/yellow]")
399
+
400
+
401
+ @click.group()
402
+ def sif():
403
+ """SIF 文件管理命令"""
404
+ pass
405
+
406
+
407
+ @sif.command(name='upload')
408
+ @click.argument('sif_file', type=click.Path(exists=True))
409
+ @click.argument('image_url')
410
+ @click.option('--username', help='Docker 仓库用户名')
411
+ @click.option('--password', help='Docker 仓库密码')
412
+ @click.option('--chunk-size', help='覆盖自适应计算的切片大小(如 100M, 500M)')
413
+ @click.option('--keep-temp', is_flag=True, help='保留临时文件')
414
+ def upload(sif_file, image_url, username, password, chunk_size, keep_temp):
415
+ """将 SIF 文件切片并构建 Docker 镜像推送到仓库
416
+
417
+ 切片策略:
418
+ - 文件 < 500MB:不切片,直接使用原文件
419
+ - 文件 500MB-2GB:切片大小 100MB
420
+ - 文件 2GB-10GB:切片大小 500MB
421
+ - 文件 > 10GB:切片大小 1GB
422
+
423
+ 可通过 --chunk-size 参数覆盖自适应策略
424
+
425
+ 示例:
426
+ adam-cli sif upload ./xxx.sif xxx.cn-hangzhou.cr.aliyuncs.com/openscore/openscore-core:1.0.0
427
+ adam-cli sif upload ./xxx.sif registry.example.com/myimage:latest --username user --password pass
428
+ """
429
+ sif_path = Path(sif_file).resolve()
430
+ file_size = sif_path.stat().st_size
431
+ file_size_mb = file_size / (1024 * 1024)
432
+
433
+ # 显示开始面板
434
+ console.print(Panel.fit(
435
+ f"[bold blue]🚀 开始构建 SIF Docker 镜像[/bold blue]\n"
436
+ f"SIF 文件: {sif_path.name}\n"
437
+ f"文件大小: {file_size_mb:.2f} MB\n"
438
+ f"目标镜像: {image_url}",
439
+ border_style="blue"
440
+ ))
441
+
442
+ work_dir = None
443
+
444
+ try:
445
+ # ===== 步骤 1: 验证环境 =====
446
+ console.print("\n[bold blue]📦 步骤 1/5: 验证环境[/bold blue]")
447
+
448
+ # 验证 SIF 文件
449
+ valid, error_msg = validateSifFile(sif_path)
450
+ if not valid:
451
+ console.print(Panel.fit(
452
+ f"[bold red]❌ SIF 文件验证失败[/bold red]\n{error_msg}",
453
+ border_style="red"
454
+ ))
455
+ sys.exit(1)
456
+ console.print(" ✓ SIF 文件可读")
457
+
458
+ # 验证镜像 URL
459
+ valid, error_msg = validateImageUrl(image_url)
460
+ if not valid:
461
+ console.print(Panel.fit(
462
+ f"[bold red]❌ 镜像 URL 验证失败[/bold red]\n{error_msg}",
463
+ border_style="red"
464
+ ))
465
+ sys.exit(1)
466
+ console.print(" ✓ 镜像 URL 格式正确")
467
+
468
+ # 检查所有必需的命令
469
+ all_available, errors = checkRequiredCommands()
470
+ if not all_available:
471
+ console.print(Panel.fit(
472
+ "[bold red]❌ 环境检查失败[/bold red]\n"
473
+ + "\n".join(errors),
474
+ border_style="red"
475
+ ))
476
+ sys.exit(1)
477
+ console.print(" ✓ split 命令可用")
478
+ console.print(" ✓ Docker 已安装并运行")
479
+
480
+ # ===== 步骤 2: 创建工作目录 =====
481
+ console.print("\n[bold blue]📦 步骤 2/5: 创建工作目录[/bold blue]")
482
+ work_dir = createWorkDir(sif_path)
483
+ console.print(f" ✓ 工作目录: {work_dir}")
484
+
485
+ # ===== 步骤 3: 切片 SIF 文件 =====
486
+ console.print("\n[bold blue]📦 步骤 3/5: 切片 SIF 文件[/bold blue]")
487
+
488
+ # 计算切片大小
489
+ if chunk_size:
490
+ actual_chunk_size = chunk_size
491
+ console.print(f" 切片策略: 自定义 ({chunk_size})")
492
+ else:
493
+ actual_chunk_size = calculateOptimalChunkSize(file_size)
494
+ if actual_chunk_size:
495
+ console.print(f" 切片策略: 自适应 ({actual_chunk_size})")
496
+ else:
497
+ console.print(f" 切片策略: 不切片 (文件 < 500MB)")
498
+
499
+ console.print(f" 文件大小: {file_size_mb:.2f} MB")
500
+
501
+ # 执行切片
502
+ try:
503
+ chunks = splitSifFile(sif_path, actual_chunk_size, work_dir)
504
+ console.print(f" ✓ 切片完成: {len(chunks)} 个文件")
505
+
506
+ # 显示切片详情
507
+ console.print(f"\n 📄 切片详情:")
508
+ for chunk in chunks:
509
+ chunk_size_mb = chunk.stat().st_size / (1024 * 1024)
510
+ console.print(f" - {chunk.name} ({chunk_size_mb:.2f} MB)")
511
+ except subprocess.CalledProcessError as e:
512
+ console.print(Panel.fit(
513
+ f"[bold red]❌ SIF 文件切片失败[/bold red]\n"
514
+ f"错误: {e.stderr}",
515
+ border_style="red"
516
+ ))
517
+ cleanupTempFiles(work_dir, keep_temp, console)
518
+ sys.exit(1)
519
+
520
+ # ===== 步骤 4: 生成 Dockerfile =====
521
+ console.print("\n[bold blue]📦 步骤 4/5: 生成 Dockerfile[/bold blue]")
522
+ dockerfile_path = generateDockerfile(work_dir)
523
+ console.print(f" ✓ Dockerfile 已生成")
524
+
525
+ # ===== 步骤 5: 构建 Docker 镜像 =====
526
+ console.print("\n[bold blue]📦 步骤 5/5: 构建 Docker 镜像[/bold blue]")
527
+
528
+ if not buildDockerImage(work_dir, image_url, console):
529
+ console.print(Panel.fit(
530
+ f"[bold red]❌ Docker 镜像构建失败[/bold red]",
531
+ border_style="red"
532
+ ))
533
+ cleanupTempFiles(work_dir, keep_temp, console)
534
+ sys.exit(1)
535
+
536
+ console.print("[green] ✓ 镜像构建成功[/green]")
537
+
538
+ # ===== 步骤 6: 推送 Docker 镜像 =====
539
+ console.print("\n[bold blue]📦 步骤 6/6: 推送 Docker 镜像[/bold blue]")
540
+
541
+ if not pushDockerImage(image_url, username, password, console):
542
+ console.print(Panel.fit(
543
+ f"[bold red]❌ Docker 镜像推送失败[/bold red]\n"
544
+ f"请检查网络连接和仓库认证信息",
545
+ border_style="red"
546
+ ))
547
+ cleanupTempFiles(work_dir, keep_temp, console)
548
+ sys.exit(1)
549
+
550
+ console.print("[green] ✓ 镜像推送成功[/green]")
551
+
552
+ # ===== 成功 =====
553
+ console.print(Panel.fit(
554
+ f"[bold green]✅ 构建成功![/bold green]\n"
555
+ f"镜像: {image_url}\n"
556
+ f"大小: {file_size_mb:.2f} MB\n"
557
+ f"切片数: {len(chunks)}",
558
+ border_style="green"
559
+ ))
560
+
561
+ # 清理临时文件
562
+ cleanupTempFiles(work_dir, keep_temp, console)
563
+
564
+ except Exception as e:
565
+ console.print(Panel.fit(
566
+ f"[bold red]❌ 执行过程中出现异常[/bold red]\n"
567
+ f"错误: {str(e)}",
568
+ border_style="red"
569
+ ))
570
+ if work_dir:
571
+ cleanupTempFiles(work_dir, keep_temp, console)
572
+ sys.exit(1)
573
+
574
+
575
+ if __name__ == '__main__':
576
+ sifBuild()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.2
2
2
  Name: adam_community
3
- Version: 1.0.25
3
+ Version: 1.0.26
4
4
  Summary: Adam Community Tools and Utilities
5
5
  Home-page: https://github.com/yourusername/adam-community
6
6
  Author: Adam Community
@@ -67,6 +67,23 @@ adam-cli build .
67
67
  adam-cli update
68
68
  ```
69
69
 
70
+ #### SIF 文件管理
71
+
72
+ 管理 SIF 文件,包括上传到 Docker 镜像仓库等操作:
73
+
74
+ ```bash
75
+ # 查看帮助
76
+ adam-cli sif --help
77
+
78
+ # 上传 SIF 文件到镜像仓库(自适应切片)
79
+ adam-cli sif upload ./xxx.sif registry.example.com/image:1.0.0
80
+
81
+ # 带认证上传
82
+ adam-cli sif upload ./app.sif registry.cn-hangzhou.aliyuncs.com/ns/app:latest \
83
+ --username user --password pass
84
+
85
+ ```
86
+
70
87
  ### Python 模块导入
71
88
 
72
89
  ```python
@@ -116,6 +133,7 @@ trackPath("/path/to/cache")
116
133
  - **项目构建**: 检查配置文件、文档文件并创建 zip 包
117
134
  - **类型检查**: 支持多种 Python 类型注解格式
118
135
  - **自动更新**: 智能检查和更新到最新版本,支持用户配置
136
+ - **SIF 镜像构建**: 将 SIF 文件切片并构建 Docker 镜像推送到仓库
119
137
 
120
138
  ## 开发
121
139
 
@@ -15,6 +15,7 @@ adam_community/cli/build.py
15
15
  adam_community/cli/cli.py
16
16
  adam_community/cli/init.py
17
17
  adam_community/cli/parser.py
18
+ adam_community/cli/sif_build.py
18
19
  adam_community/cli/updater.py
19
20
  adam_community/cli/templates/Makefile.j2
20
21
  adam_community/cli/templates/README_agent.md.j2
@@ -31,4 +32,5 @@ adam_community/cli/templates/kit_python.py.j2
31
32
  adam_community/cli/templates/long_description.md.j2
32
33
  adam_community/cli/templates/long_description_en.md.j2
33
34
  adam_community/cli/templates/rag_python.py.j2
35
+ test/test_sif_upload.py
34
36
  test/test_util_tool.py
@@ -0,0 +1,444 @@
1
+ import unittest
2
+ from unittest.mock import patch, MagicMock, call
3
+ from unittest.mock import mock_open
4
+ import tempfile
5
+ import shutil
6
+ import os
7
+ from pathlib import Path
8
+
9
+ # 添加项目根目录到 Python 路径
10
+ import sys
11
+ sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
12
+
13
+ from adam_community.cli.sif_build import (
14
+ validateSifFile,
15
+ validateImageUrl,
16
+ checkCommandAvailable,
17
+ checkDockerEnvironment,
18
+ checkRequiredCommands,
19
+ createWorkDir,
20
+ calculateOptimalChunkSize,
21
+ splitSifFile,
22
+ generateDockerfile,
23
+ )
24
+
25
+
26
+ class TestValidateSifFile(unittest.TestCase):
27
+ """测试 SIF 文件验证功能"""
28
+
29
+ def setUp(self):
30
+ self.temp_dir = tempfile.mkdtemp()
31
+
32
+ def tearDown(self):
33
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
34
+
35
+ def test_valid_sif_file(self):
36
+ """测试有效的 SIF 文件"""
37
+ # 创建测试文件
38
+ test_file = Path(self.temp_dir) / "test.sif"
39
+ test_file.write_text("test content")
40
+
41
+ # 验证文件
42
+ valid, error_msg = validateSifFile(test_file)
43
+
44
+ # 断言
45
+ self.assertTrue(valid)
46
+ self.assertEqual(error_msg, "")
47
+
48
+ def test_file_not_exists(self):
49
+ """测试文件不存在"""
50
+ test_file = Path(self.temp_dir) / "nonexistent.sif"
51
+
52
+ valid, error_msg = validateSifFile(test_file)
53
+
54
+ self.assertFalse(valid)
55
+ self.assertIn("不存在", error_msg)
56
+
57
+ def test_empty_file(self):
58
+ """测试空文件"""
59
+ test_file = Path(self.temp_dir) / "empty.sif"
60
+ test_file.touch()
61
+
62
+ valid, error_msg = validateSifFile(test_file)
63
+
64
+ self.assertFalse(valid)
65
+ self.assertIn("空", error_msg)
66
+
67
+
68
+ class TestValidateImageUrl(unittest.TestCase):
69
+ """测试镜像 URL 验证功能"""
70
+
71
+ def test_valid_urls(self):
72
+ """测试有效的镜像 URL"""
73
+ valid_urls = [
74
+ "registry.example.com/namespace/image:1.0.0",
75
+ "xxx.cn-hangzhou.cr.aliyuncs.com/openscore/openscore-core:1.0.0",
76
+ "docker.io/library/ubuntu:20.04",
77
+ "registry.com/org/image:latest",
78
+ "127.0.0.1:5000/myimage:tag1"
79
+ ]
80
+
81
+ for url in valid_urls:
82
+ with self.subTest(url=url):
83
+ valid, error_msg = validateImageUrl(url)
84
+ self.assertTrue(valid, f"URL 应该有效: {url}")
85
+ self.assertEqual(error_msg, "")
86
+
87
+ def test_invalid_urls(self):
88
+ """测试无效的镜像 URL"""
89
+ invalid_urls = [
90
+ "invalid-url",
91
+ "registry.com/image",
92
+ "registry.com/image:",
93
+ "image:tag",
94
+ ""
95
+ ]
96
+
97
+ for url in invalid_urls:
98
+ with self.subTest(url=url):
99
+ valid, error_msg = validateImageUrl(url)
100
+ self.assertFalse(valid, f"URL 应该无效: {url}")
101
+ self.assertIn("格式不正确", error_msg)
102
+
103
+
104
+ class TestCheckCommandAvailable(unittest.TestCase):
105
+ """测试命令可用性检查"""
106
+
107
+ def test_which_command_available(self):
108
+ """测试 which 命令存在时"""
109
+ # 在大多数系统上,ls 应该存在
110
+ with patch('subprocess.run') as mock_run:
111
+ mock_run.return_value = MagicMock(
112
+ returncode=0,
113
+ stdout="/bin/ls\n"
114
+ )
115
+
116
+ valid, hint, url = checkCommandAvailable("ls")
117
+
118
+ self.assertTrue(valid)
119
+ self.assertEqual(hint, "")
120
+ self.assertEqual(url, "")
121
+
122
+ def test_command_not_found(self):
123
+ """测试命令不存在"""
124
+ with patch('subprocess.run') as mock_run:
125
+ # 模拟 which 找不到命令
126
+ mock_run.side_effect = FileNotFoundError("command not found")
127
+
128
+ # 测试 split 命令提示
129
+ valid, hint, url = checkCommandAvailable("nonexistent_cmd")
130
+
131
+ self.assertFalse(valid)
132
+ # 不在预定义列表中,返回通用提示
133
+ self.assertIn("未找到", hint)
134
+
135
+
136
+ class TestCheckDockerEnvironment(unittest.TestCase):
137
+ """测试 Docker 环境检查"""
138
+
139
+ def test_docker_available_and_running(self):
140
+ """测试 Docker 可用且正在运行"""
141
+ with patch('subprocess.run') as mock_run:
142
+ # which 找到 docker
143
+ # docker info 成功
144
+ mock_run.side_effect = [
145
+ MagicMock(returncode=0, stdout="/usr/bin/docker\n"),
146
+ MagicMock(returncode=0, stdout="Docker is running\n")
147
+ ]
148
+
149
+ valid, errors = checkDockerEnvironment()
150
+
151
+ self.assertTrue(valid)
152
+ self.assertEqual(len(errors), 0)
153
+
154
+ def test_docker_not_installed(self):
155
+ """测试 Docker 未安装"""
156
+ with patch('subprocess.run') as mock_run:
157
+ # which 找不到 docker
158
+ mock_run.side_effect = FileNotFoundError("docker not found")
159
+
160
+ valid, errors = checkDockerEnvironment()
161
+
162
+ self.assertFalse(valid)
163
+ self.assertGreater(len(errors), 0)
164
+ self.assertIn("Docker", errors[0])
165
+
166
+ def test_docker_not_running(self):
167
+ """测试 Docker 已安装但未运行"""
168
+ with patch('subprocess.run') as mock_run:
169
+ # which 找到 docker
170
+ # 但 docker info 失败
171
+ mock_run.side_effect = [
172
+ MagicMock(returncode=0, stdout="/usr/bin/docker\n"),
173
+ MagicMock(returncode=1, stderr="Cannot connect to daemon\n")
174
+ ]
175
+
176
+ valid, errors = checkDockerEnvironment()
177
+
178
+ self.assertFalse(valid)
179
+ self.assertGreater(len(errors), 0)
180
+ # 检查是否包含 daemon 提示
181
+ error_text = "".join(errors)
182
+ self.assertIn("daemon", error_text.lower())
183
+
184
+
185
+ class TestCheckRequiredCommands(unittest.TestCase):
186
+ """测试所有必需命令检查"""
187
+
188
+ def test_all_commands_available(self):
189
+ """测试所有命令都可用"""
190
+ with patch('adam_community.cli.sif_build.checkCommandAvailable') as mock_check:
191
+ with patch('adam_community.cli.sif_build.checkDockerEnvironment') as mock_docker:
192
+ # split 和 docker 都可用
193
+ mock_check.return_value = (True, "", "")
194
+ mock_docker.return_value = (True, [])
195
+
196
+ valid, errors = checkRequiredCommands()
197
+
198
+ self.assertTrue(valid)
199
+ self.assertEqual(len(errors), 0)
200
+
201
+ def test_split_missing(self):
202
+ """测试 split 命令缺失"""
203
+ with patch('adam_community.cli.sif_build.checkCommandAvailable') as mock_check:
204
+ # split 不可用
205
+ mock_check.side_effect = [
206
+ (False, "split 命令未找到", "安装提示\nhttps://example.com"),
207
+ (True, "", "") # docker 可用
208
+ ]
209
+
210
+ valid, errors = checkRequiredCommands()
211
+
212
+ self.assertFalse(valid)
213
+ self.assertGreater(len(errors), 0)
214
+ self.assertIn("split", errors[0])
215
+
216
+ def test_docker_missing(self):
217
+ """测试 Docker 缺失"""
218
+ with patch('adam_community.cli.sif_build.checkCommandAvailable') as mock_check:
219
+ # split 可用,docker 不可用
220
+ mock_check.side_effect = [
221
+ (True, "", ""), # split 可用
222
+ (False, "Docker 未安装", "安装 Docker\nhttps://docker.com")
223
+ ]
224
+
225
+ valid, errors = checkRequiredCommands()
226
+
227
+ self.assertFalse(valid)
228
+ self.assertGreater(len(errors), 0)
229
+
230
+
231
+ class TestCalculateOptimalChunkSize(unittest.TestCase):
232
+ """测试自适应切片大小计算"""
233
+
234
+ def test_no_chunking_small_file(self):
235
+ """测试小文件不切片"""
236
+ # 400MB
237
+ result = calculateOptimalChunkSize(400 * 1024 * 1024)
238
+ self.assertIsNone(result)
239
+
240
+ # 499MB
241
+ result = calculateOptimalChunkSize(499 * 1024 * 1024)
242
+ self.assertIsNone(result)
243
+
244
+ def test_100mb_chunking(self):
245
+ """测试 100MB 切片"""
246
+ # 500MB
247
+ result = calculateOptimalChunkSize(500 * 1024 * 1024)
248
+ self.assertEqual(result, "100M")
249
+
250
+ # 1GB
251
+ result = calculateOptimalChunkSize(1024 * 1024 * 1024)
252
+ self.assertEqual(result, "100M")
253
+
254
+ # 1.5GB
255
+ result = calculateOptimalChunkSize(int(1.5 * 1024 * 1024 * 1024))
256
+ self.assertEqual(result, "100M")
257
+
258
+ def test_500mb_chunking(self):
259
+ """测试 500MB 切片"""
260
+ # 2GB
261
+ result = calculateOptimalChunkSize(2 * 1024 * 1024 * 1024)
262
+ self.assertEqual(result, "500M")
263
+
264
+ # 5GB
265
+ result = calculateOptimalChunkSize(5 * 1024 * 1024 * 1024)
266
+ self.assertEqual(result, "500M")
267
+
268
+ # 9.9GB
269
+ result = calculateOptimalChunkSize(int(9.9 * 1024 * 1024 * 1024))
270
+ self.assertEqual(result, "500M")
271
+
272
+ def test_1gb_chunking(self):
273
+ """测试 1GB 切片"""
274
+ # 10GB
275
+ result = calculateOptimalChunkSize(10 * 1024 * 1024 * 1024)
276
+ self.assertEqual(result, "1G")
277
+
278
+ # 20GB
279
+ result = calculateOptimalChunkSize(20 * 1024 * 1024 * 1024)
280
+ self.assertEqual(result, "1G")
281
+
282
+
283
+ class TestCreateWorkDir(unittest.TestCase):
284
+ """测试工作目录创建"""
285
+
286
+ def setUp(self):
287
+ self.temp_dir = tempfile.mkdtemp()
288
+
289
+ def tearDown(self):
290
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
291
+
292
+ def test_create_work_dir(self):
293
+ """测试创建工作目录"""
294
+ sif_path = Path(self.temp_dir) / "test.sif"
295
+ sif_path.touch()
296
+
297
+ work_dir = createWorkDir(sif_path)
298
+
299
+ # 验证目录已创建
300
+ self.assertTrue(work_dir.exists())
301
+ self.assertTrue(work_dir.is_dir())
302
+
303
+ # 验证目录名称
304
+ self.assertEqual(work_dir.name, ".sif_build_temp")
305
+
306
+ # 验证目录位置(在 SIF 文件同目录下)
307
+ self.assertEqual(work_dir.parent, sif_path.parent)
308
+
309
+
310
+ class TestGenerateDockerfile(unittest.TestCase):
311
+ """测试 Dockerfile 生成"""
312
+
313
+ def setUp(self):
314
+ self.temp_dir = tempfile.mkdtemp()
315
+
316
+ def tearDown(self):
317
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
318
+
319
+ def test_generate_dockerfile(self):
320
+ """测试生成 Dockerfile"""
321
+ work_dir = Path(self.temp_dir)
322
+
323
+ dockerfile_path = generateDockerfile(work_dir)
324
+
325
+ # 验证文件已创建
326
+ self.assertTrue(dockerfile_path.exists())
327
+ self.assertEqual(dockerfile_path.name, "Dockerfile")
328
+
329
+ # 读取并验证内容
330
+ with open(dockerfile_path, 'r') as f:
331
+ content = f.read()
332
+
333
+ self.assertIn("FROM alpine", content)
334
+ self.assertIn("COPY . /sif", content)
335
+
336
+
337
+ class TestSplitSifFile(unittest.TestCase):
338
+ """测试 SIF 文件切片"""
339
+
340
+ def setUp(self):
341
+ self.temp_dir = tempfile.mkdtemp()
342
+ # 创建测试 SIF 文件(10MB)
343
+ self.sif_path = Path(self.temp_dir) / "test.sif"
344
+ with open(self.sif_path, 'wb') as f:
345
+ f.write(b'0' * (10 * 1024 * 1024)) # 10MB
346
+
347
+ def tearDown(self):
348
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
349
+
350
+ @patch('subprocess.run')
351
+ def test_split_with_chunk_size(self, mock_run):
352
+ """测试带切片大小的切片"""
353
+ work_dir = Path(self.temp_dir)
354
+
355
+ # 模拟 split 命令成功
356
+ mock_run.return_value = MagicMock(returncode=0)
357
+
358
+ chunks = splitSifFile(self.sif_path, "100M", work_dir)
359
+
360
+ # 验证调用了 split 命令
361
+ mock_run.assert_called_once()
362
+ cmd = mock_run.call_args[0][0]
363
+ self.assertEqual(cmd[0], "split")
364
+ self.assertIn("-b", cmd)
365
+ self.assertIn("100M", cmd)
366
+ self.assertIn("-d", cmd)
367
+
368
+ def test_no_chunking(self):
369
+ """测试不切片(chunk_size=None)"""
370
+ # Create a separate work directory to avoid SameFileError
371
+ work_dir = Path(self.temp_dir) / "work"
372
+ work_dir.mkdir()
373
+
374
+ chunks = splitSifFile(self.sif_path, None, work_dir)
375
+
376
+ # 应该只有一个文件(原文件的副本)
377
+ self.assertEqual(len(chunks), 1)
378
+ self.assertEqual(chunks[0].name, "test.sif")
379
+ self.assertTrue(chunks[0].exists())
380
+
381
+ # 验证文件大小
382
+ self.assertEqual(chunks[0].stat().st_size, 10 * 1024 * 1024)
383
+
384
+
385
+ class TestIntegration(unittest.TestCase):
386
+ """集成测试"""
387
+
388
+ def setUp(self):
389
+ self.temp_dir = tempfile.mkdtemp()
390
+
391
+ def tearDown(self):
392
+ shutil.rmtree(self.temp_dir, ignore_errors=True)
393
+
394
+ def test_full_workflow_small_file(self):
395
+ """测试小文件的完整工作流(不切片)"""
396
+ # 1. 创建 SIF 文件
397
+ sif_path = Path(self.temp_dir) / "test.sif"
398
+ file_size = 5 * 1024 * 1024 # 5MB
399
+ with open(sif_path, 'wb') as f:
400
+ f.write(b'0' * file_size)
401
+
402
+ # 2. 验证文件
403
+ valid, error = validateSifFile(sif_path)
404
+ self.assertTrue(valid, error)
405
+
406
+ # 3. 计算切片大小(应该不切片)
407
+ chunk_size = calculateOptimalChunkSize(file_size)
408
+ self.assertIsNone(chunk_size)
409
+
410
+ # 4. 创建工作目录
411
+ work_dir = createWorkDir(sif_path)
412
+ self.assertTrue(work_dir.exists())
413
+
414
+ # 5. 切片文件(不切片)
415
+ chunks = splitSifFile(sif_path, chunk_size, work_dir)
416
+ self.assertEqual(len(chunks), 1)
417
+
418
+ # 6. 生成 Dockerfile
419
+ dockerfile_path = generateDockerfile(work_dir)
420
+ self.assertTrue(dockerfile_path.exists())
421
+
422
+ # 7. 验证 Dockerfile 内容
423
+ with open(dockerfile_path, 'r') as f:
424
+ content = f.read()
425
+ self.assertIn("FROM alpine", content)
426
+ self.assertIn("COPY . /sif", content)
427
+
428
+ def test_chunk_size_boundaries(self):
429
+ """测试切片大小的边界值"""
430
+ test_cases = [
431
+ (499 * 1024 * 1024, None), # 499MB -> 不切片
432
+ (500 * 1024 * 1024, "100M"), # 500MB -> 100M
433
+ (2 * 1024 * 1024 * 1024, "500M"), # 2GB -> 500M
434
+ (10 * 1024 * 1024 * 1024, "1G"), # 10GB -> 1G
435
+ ]
436
+
437
+ for file_size, expected_chunk in test_cases:
438
+ with self.subTest(file_size=file_size):
439
+ result = calculateOptimalChunkSize(file_size)
440
+ self.assertEqual(result, expected_chunk)
441
+
442
+
443
+ if __name__ == '__main__':
444
+ unittest.main()
@@ -1 +0,0 @@
1
- __version__ = "1.0.25"