atomgit 1.0.5__tar.gz → 1.0.7__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomgit
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Summary: AtomGit模型文件上传下载CLI工具
5
5
  Home-page: https://atomgit.com/gitcode-ai/atomgit_cli
6
6
  Author: AtomGit CLI Team
@@ -8,7 +8,7 @@ AtomGit CLI - 基于Transformers和Hugging Face Hub的模型文件上传下载
8
8
  支持模型和数据集的上传、下载等操作。
9
9
  """
10
10
 
11
- __version__ = '1.0.5'
11
+ __version__ = '1.0.7'
12
12
  __author__ = 'AtomGit CLI Team'
13
13
  __description__ = 'AtomGit模型文件上传下载CLI工具'
14
14
 
@@ -16,6 +16,12 @@ os.environ["HF_HOME"] = cache_dir
16
16
 
17
17
 
18
18
  from huggingface_hub import hf_hub_download, upload_folder, create_repo, snapshot_download, constants as hf_constants
19
+ try:
20
+ from .rate_limiter import install_hf_rate_limiter
21
+ except ImportError:
22
+ from rate_limiter import install_hf_rate_limiter
23
+
24
+ install_hf_rate_limiter()
19
25
 
20
26
  try:
21
27
  from .config import config
@@ -221,7 +227,8 @@ class HuggingFaceAPI:
221
227
  repo_id=normalized_repo_id,
222
228
  local_dir=str(local_path),
223
229
  force_download=force_download, # 根据用户选择决定是否强制下载
224
- token=credentials['token'] if credentials and 'token' in credentials else None
230
+ token=credentials['token'] if credentials and 'token' in credentials else None,
231
+ max_workers=8
225
232
  )
226
233
  print(f"✅ 仓库下载成功")
227
234
  return True
@@ -247,11 +254,14 @@ class HuggingFaceAPI:
247
254
  local_path.mkdir(parents=True, exist_ok=True)
248
255
 
249
256
  # 首先尝试不使用token下载(适用于公开仓库)
257
+ credentials = config.get_credentials()
250
258
  try:
251
259
  hf_hub_download(
252
260
  repo_id=normalized_repo_id,
253
261
  filename=filename,
254
- local_dir=str(local_path)
262
+ local_dir=str(local_path),
263
+ token=credentials['token'] if credentials else None,
264
+ force_download=force_download
255
265
  )
256
266
  print(f"✅ 文件下载成功")
257
267
  return True
@@ -264,7 +274,6 @@ class HuggingFaceAPI:
264
274
  print("检测到认证问题,尝试使用token下载...")
265
275
 
266
276
  # 如果公开下载失败,尝试使用token下载
267
- credentials = config.get_credentials()
268
277
  if credentials:
269
278
  try:
270
279
  hf_hub_download(
@@ -307,4 +316,4 @@ class HuggingFaceAPI:
307
316
 
308
317
 
309
318
  # 全局API实例
310
- api = HuggingFaceAPI()
319
+ api = HuggingFaceAPI()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomgit
3
- Version: 1.0.5
3
+ Version: 1.0.7
4
4
  Summary: AtomGit模型文件上传下载CLI工具
5
5
  Home-page: https://atomgit.com/gitcode-ai/atomgit_cli
6
6
  Author: AtomGit CLI Team
@@ -6,6 +6,7 @@ api.py
6
6
  atomgit_hub.py
7
7
  cli.py
8
8
  config.py
9
+ rate_limiter.py
9
10
  requirements.txt
10
11
  setup.py
11
12
  test.py
@@ -16,6 +17,7 @@ utils.py
16
17
  ./atomgit_hub.py
17
18
  ./cli.py
18
19
  ./config.py
20
+ ./rate_limiter.py
19
21
  ./test.py
20
22
  ./utils.py
21
23
  atomgit.egg-info/PKG-INFO
@@ -23,4 +25,5 @@ atomgit.egg-info/SOURCES.txt
23
25
  atomgit.egg-info/dependency_links.txt
24
26
  atomgit.egg-info/entry_points.txt
25
27
  atomgit.egg-info/requires.txt
26
- atomgit.egg-info/top_level.txt
28
+ atomgit.egg-info/top_level.txt
29
+ tests/test_rate_limiter.py
@@ -23,6 +23,15 @@ os.environ["HF_HOME"] = cache_dir
23
23
 
24
24
  from huggingface_hub import snapshot_download as hf_snapshot_download
25
25
  from huggingface_hub import hf_hub_download, upload_folder as hf_upload_folder, create_repo
26
+ try:
27
+ from .rate_limiter import install_hf_rate_limiter
28
+ except ImportError:
29
+ try:
30
+ from rate_limiter import install_hf_rate_limiter
31
+ except ImportError:
32
+ from atomgit.rate_limiter import install_hf_rate_limiter
33
+
34
+ install_hf_rate_limiter()
26
35
 
27
36
 
28
37
  try:
@@ -331,8 +340,10 @@ def upload_folder(
331
340
  # 如果要上传到根目录,直接使用源文件夹
332
341
  upload_path = str(folder_path)
333
342
  else:
334
- # 如果要上传到特定路径,需要重新组织目录结构
335
- with tempfile.TemporaryDirectory() as temp_dir:
343
+ # 如果要上传到特定路径,需要重新组织目录结构。
344
+ # 注意:不能提前退出 with TemporaryDirectory(),否则上传时临时目录已被删除。
345
+ temp_dir = tempfile.mkdtemp(prefix="atomgit_upload_")
346
+ try:
336
347
  temp_path = Path(temp_dir)
337
348
 
338
349
  # 创建目标路径
@@ -342,7 +353,11 @@ def upload_folder(
342
353
  # 复制整个目录树
343
354
  shutil.copytree(folder_path, target_path, dirs_exist_ok=True)
344
355
 
345
- upload_path = str(temp_path)
356
+ except Exception:
357
+ shutil.rmtree(temp_dir, ignore_errors=True)
358
+ raise
359
+
360
+ upload_path = str(temp_path)
346
361
  # 使用 Monkey Patch 方式临时修改 huggingface_hub 的默认超时配置
347
362
  from huggingface_hub import constants as hf_constants
348
363
 
@@ -372,6 +387,10 @@ def upload_folder(
372
387
  raise Exception(f"仓库不存在:{repo_id}")
373
388
  else:
374
389
  raise Exception(f"上传失败:{error_msg}")
390
+ finally:
391
+ # 清理临时目录(仅重新组织目录结构时创建)
392
+ if path_in_repo not in ("./", ".", ""):
393
+ shutil.rmtree(temp_dir, ignore_errors=True)
375
394
 
376
395
 
377
396
  def create_repository(
@@ -39,7 +39,7 @@ except ImportError:
39
39
 
40
40
 
41
41
  @click.group()
42
- @click.version_option(version='1.0.5')
42
+ @click.version_option(version='1.0.7')
43
43
  def cli():
44
44
  """AtomGit CLI - 基于Transformers和Hugging Face Hub的AtomGit平台模型文件上传下载工具"""
45
45
  pass
@@ -0,0 +1,107 @@
1
+ """Shared client-side rate limiting for AtomGit Hub requests."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import time
8
+ import threading
9
+ from pathlib import Path
10
+ from typing import Optional
11
+ from urllib.parse import urlparse
12
+
13
+
14
+ class SharedRateLimiter:
15
+ """A cross-process leaky bucket using a small lock-protected state file."""
16
+
17
+ _thread_lock = threading.Lock()
18
+
19
+ def __init__(self, rate_per_minute: float = 45.0, burst: int = 2,
20
+ state_file: Optional[Path] = None) -> None:
21
+ self.rate = max(1.0, rate_per_minute) / 60.0
22
+ self.burst = max(1, burst)
23
+ self.state_file = state_file or (
24
+ Path(os.path.expanduser("~/.cache/atomgit")) / "rate_limit.json"
25
+ )
26
+ self.state_file.parent.mkdir(parents=True, exist_ok=True)
27
+
28
+ def acquire(self) -> None:
29
+ """Reserve one request slot, sharing reservations across processes."""
30
+ while True:
31
+ wait_for = 0.0
32
+ # flock is process-scoped on some Unix systems; the thread lock is
33
+ # therefore also required when a single CLI uses a worker pool.
34
+ with self._thread_lock, self.state_file.open("a+", encoding="utf-8") as handle:
35
+ try:
36
+ import fcntl
37
+ fcntl.flock(handle.fileno(), fcntl.LOCK_EX)
38
+ except ImportError:
39
+ pass
40
+ handle.seek(0)
41
+ try:
42
+ state = json.load(handle)
43
+ except (json.JSONDecodeError, EOFError):
44
+ state = {"next_slot": 0.0}
45
+
46
+ now = time.monotonic()
47
+ next_slot = float(state.get("next_slot", 0.0))
48
+ # A reservation may be at most ``burst`` intervals ahead.
49
+ reservation = max(next_slot, now - self.burst / self.rate)
50
+ reservation += 1.0 / self.rate
51
+ state["next_slot"] = reservation
52
+ handle.seek(0)
53
+ handle.truncate()
54
+ json.dump(state, handle)
55
+ handle.flush()
56
+ wait_for = max(0.0, reservation - now)
57
+ try:
58
+ fcntl.flock(handle.fileno(), fcntl.LOCK_UN)
59
+ except (NameError, UnboundLocalError):
60
+ pass
61
+ if wait_for <= 0:
62
+ return
63
+ time.sleep(min(wait_for, 1.0))
64
+
65
+
66
+ def install_hf_rate_limiter() -> bool:
67
+ """Install the limiter into huggingface_hub v1.x when available.
68
+
69
+ The function is intentionally best-effort so the CLI remains compatible
70
+ with older huggingface_hub releases.
71
+ """
72
+ try:
73
+ import httpx
74
+ from huggingface_hub import set_client_factory
75
+ except (ImportError, AttributeError):
76
+ return False
77
+
78
+ # AtomGit's file metadata endpoint is limited to 50 requests/minute.
79
+ # Keep a safety margin and leave ordinary file transfer requests untouched.
80
+ limiter = SharedRateLimiter(rate_per_minute=45.0, burst=2)
81
+
82
+ def is_file_api_request(request) -> bool:
83
+ path = urlparse(str(request.url)).path.rstrip("/")
84
+ return "/api/v1/file/" in f"{path}/"
85
+
86
+ class LimitedClient(httpx.Client):
87
+ def send(self, request, *args, **kwargs):
88
+ attempts = 0
89
+ while True:
90
+ limited = is_file_api_request(request)
91
+ if limited:
92
+ limiter.acquire()
93
+ response = super().send(request, *args, **kwargs)
94
+ if not limited or response.status_code != 429 or attempts >= 5:
95
+ return response
96
+ retry_after = response.headers.get("Retry-After")
97
+ try:
98
+ delay = max(1.0, float(retry_after)) if retry_after else 2 ** attempts
99
+ except ValueError:
100
+ delay = 2 ** attempts
101
+ response.close()
102
+ time.sleep(min(delay, 60.0))
103
+ attempts += 1
104
+
105
+ set_client_factory(lambda: LimitedClient())
106
+ return True
107
+ _thread_lock = threading.Lock()
@@ -31,7 +31,7 @@ def read_requirements():
31
31
 
32
32
  setup(
33
33
  name='atomgit',
34
- version='1.0.5',
34
+ version='1.0.7',
35
35
  author='AtomGit CLI Team',
36
36
  author_email='sa@atomgit.com',
37
37
  description='AtomGit模型文件上传下载CLI工具',
@@ -130,7 +130,7 @@ def run_all_tests():
130
130
  # 2. 测试upload
131
131
  # test_upload_folder(repo_id=model, folder_path=model_folder_path)
132
132
  # test_upload_folder(repo_id=dataset, folder_path=dataset_folder_path)
133
- test_upload_folder(repo_id="yanlp/dataset-t1", folder_path="/Users/yanlp/csdn/IdeaProjects/gitcode/gitcode-hf-registry/dataset-t11")
133
+ test_upload_folder(repo_id="yanlp/dataset-t2", folder_path="/Users/yanlp/csdn/IdeaProjects/gitcode/gitcode-hf-registry/dataset-t11")
134
134
 
135
135
  # 3. 测试下载
136
136
  # test_snapshot_download(repo_id=model, local_dir="./test_downloads/model/snapshot_full")
@@ -0,0 +1,72 @@
1
+ import json
2
+ import subprocess
3
+ import sys
4
+ import tempfile
5
+ import threading
6
+ import time
7
+ import unittest
8
+ from pathlib import Path
9
+
10
+ from rate_limiter import SharedRateLimiter, install_hf_rate_limiter
11
+
12
+
13
+ class RateLimiterTests(unittest.TestCase):
14
+ def test_concurrent_access_keeps_valid_shared_state(self):
15
+ with tempfile.TemporaryDirectory() as directory:
16
+ state_file = Path(directory) / "rate.json"
17
+ limiter = SharedRateLimiter(60000, 4, state_file)
18
+ errors = []
19
+
20
+ def worker():
21
+ try:
22
+ for _ in range(10):
23
+ limiter.acquire()
24
+ except Exception as error: # pragma: no cover - assertion below
25
+ errors.append(error)
26
+
27
+ threads = [threading.Thread(target=worker) for _ in range(4)]
28
+ for thread in threads:
29
+ thread.start()
30
+ for thread in threads:
31
+ thread.join()
32
+
33
+ self.assertFalse(errors)
34
+ self.assertIsInstance(json.loads(state_file.read_text()), dict)
35
+
36
+ def test_multiple_processes_share_state_file(self):
37
+ with tempfile.TemporaryDirectory() as directory:
38
+ state_file = Path(directory) / "rate.json"
39
+ code = (
40
+ "from pathlib import Path; "
41
+ "from rate_limiter import SharedRateLimiter; "
42
+ f"SharedRateLimiter(60000, 4, Path({str(state_file)!r})).acquire()"
43
+ )
44
+ processes = [subprocess.run([sys.executable, "-c", code]) for _ in range(3)]
45
+ self.assertTrue(all(process.returncode == 0 for process in processes))
46
+ self.assertTrue(state_file.exists())
47
+
48
+ def test_huggingface_client_factory_is_limited(self):
49
+ import huggingface_hub
50
+
51
+ self.assertTrue(install_hf_rate_limiter())
52
+ self.assertEqual(type(huggingface_hub.get_session()).__name__, "LimitedClient")
53
+ huggingface_hub.close_session()
54
+
55
+ def test_file_api_limit_is_conservative(self):
56
+ limiter = SharedRateLimiter()
57
+ self.assertAlmostEqual(limiter.rate * 60, 45.0)
58
+ self.assertEqual(limiter.burst, 2)
59
+
60
+
61
+ class InstalledCliTests(unittest.TestCase):
62
+ def test_installed_cli_help(self):
63
+ cli = Path(__file__).parents[1] / ".venv" / "bin" / "atomgit"
64
+ if not cli.exists():
65
+ self.skipTest(".venv/bin/atomgit is not installed")
66
+ result = subprocess.run([str(cli), "--help"], capture_output=True, text=True)
67
+ self.assertEqual(result.returncode, 0, result.stderr)
68
+ self.assertIn("download", result.stdout)
69
+
70
+
71
+ if __name__ == "__main__":
72
+ unittest.main()
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes