atomgit 1.0.4__tar.gz → 1.0.6__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.
@@ -0,0 +1 @@
1
+ include requirements.txt
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomgit
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: AtomGit模型文件上传下载CLI工具
5
5
  Home-page: https://atomgit.com/gitcode-ai/atomgit_cli
6
6
  Author: AtomGit CLI Team
@@ -29,8 +29,8 @@ Requires-Dist: requests>=2.31.0
29
29
  Requires-Dist: tqdm>=4.66.1
30
30
  Requires-Dist: colorama>=0.4.6
31
31
  Requires-Dist: tabulate>=0.9.0
32
- Requires-Dist: huggingface-hub>=0.20.0
33
- Requires-Dist: datasets>=2.16.0
32
+ Requires-Dist: huggingface-hub==1.1.7
33
+ Requires-Dist: datasets==4.4.1
34
34
  Dynamic: author
35
35
  Dynamic: author-email
36
36
  Dynamic: classifier
@@ -8,7 +8,7 @@ AtomGit CLI - 基于Transformers和Hugging Face Hub的模型文件上传下载
8
8
  支持模型和数据集的上传、下载等操作。
9
9
  """
10
10
 
11
- __version__ = '1.0.4'
11
+ __version__ = '1.0.6'
12
12
  __author__ = 'AtomGit CLI Team'
13
13
  __description__ = 'AtomGit模型文件上传下载CLI工具'
14
14
 
@@ -7,6 +7,8 @@ import urllib.error
7
7
 
8
8
  # 设置Hugging Face Hub的API端点为AtomGit
9
9
  os.environ["HF_ENDPOINT"] = "https://hub.atomgit.com"
10
+ # 禁用Xet协议,避免 xet-write-token 请求
11
+ os.environ["HF_HUB_DISABLE_XET"] = "1"
10
12
  # 设置缓存目录
11
13
  cache_dir = os.path.expanduser("~/.cache/atomgit")
12
14
  os.makedirs(cache_dir, exist_ok=True)
@@ -14,6 +16,12 @@ os.environ["HF_HOME"] = cache_dir
14
16
 
15
17
 
16
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()
17
25
 
18
26
  try:
19
27
  from .config import config
@@ -151,12 +159,13 @@ class HuggingFaceAPI:
151
159
  # 使用 Monkey Patch 方式临时修改 huggingface_hub 的默认超时配置
152
160
  commit_message = message or "Upload folder using atomgit client"
153
161
  hf_constants.DEFAULT_REQUEST_TIMEOUT = upload_timeout
154
- upload_folder(
162
+ upload_kwargs = dict(
155
163
  repo_id=repo_id,
156
164
  folder_path=str(temp_dir),
157
165
  token=credentials['token'],
158
- commit_message=commit_message
166
+ commit_message=commit_message,
159
167
  )
168
+ upload_folder(**upload_kwargs)
160
169
 
161
170
  return True
162
171
  finally:
@@ -185,12 +194,13 @@ class HuggingFaceAPI:
185
194
  # 使用 Monkey Patch 方式临时修改 huggingface_hub 的默认超时配置
186
195
  commit_message = message or "Upload folder using atomgit client"
187
196
  hf_constants.DEFAULT_REQUEST_TIMEOUT = upload_timeout
188
- upload_folder(
197
+ upload_kwargs = dict(
189
198
  repo_id=repo_id,
190
199
  folder_path=str(dir_path),
191
200
  token=credentials['token'],
192
- commit_message=commit_message
201
+ commit_message=commit_message,
193
202
  )
203
+ upload_folder(**upload_kwargs)
194
204
 
195
205
  return True
196
206
 
@@ -217,7 +227,8 @@ class HuggingFaceAPI:
217
227
  repo_id=normalized_repo_id,
218
228
  local_dir=str(local_path),
219
229
  force_download=force_download, # 根据用户选择决定是否强制下载
220
- 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
221
232
  )
222
233
  print(f"✅ 仓库下载成功")
223
234
  return True
@@ -243,11 +254,14 @@ class HuggingFaceAPI:
243
254
  local_path.mkdir(parents=True, exist_ok=True)
244
255
 
245
256
  # 首先尝试不使用token下载(适用于公开仓库)
257
+ credentials = config.get_credentials()
246
258
  try:
247
259
  hf_hub_download(
248
260
  repo_id=normalized_repo_id,
249
261
  filename=filename,
250
- local_dir=str(local_path)
262
+ local_dir=str(local_path),
263
+ token=credentials['token'] if credentials else None,
264
+ force_download=force_download
251
265
  )
252
266
  print(f"✅ 文件下载成功")
253
267
  return True
@@ -260,7 +274,6 @@ class HuggingFaceAPI:
260
274
  print("检测到认证问题,尝试使用token下载...")
261
275
 
262
276
  # 如果公开下载失败,尝试使用token下载
263
- credentials = config.get_credentials()
264
277
  if credentials:
265
278
  try:
266
279
  hf_hub_download(
@@ -303,4 +316,4 @@ class HuggingFaceAPI:
303
316
 
304
317
 
305
318
  # 全局API实例
306
- api = HuggingFaceAPI()
319
+ api = HuggingFaceAPI()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: atomgit
3
- Version: 1.0.4
3
+ Version: 1.0.6
4
4
  Summary: AtomGit模型文件上传下载CLI工具
5
5
  Home-page: https://atomgit.com/gitcode-ai/atomgit_cli
6
6
  Author: AtomGit CLI Team
@@ -29,8 +29,8 @@ Requires-Dist: requests>=2.31.0
29
29
  Requires-Dist: tqdm>=4.66.1
30
30
  Requires-Dist: colorama>=0.4.6
31
31
  Requires-Dist: tabulate>=0.9.0
32
- Requires-Dist: huggingface-hub>=0.20.0
33
- Requires-Dist: datasets>=2.16.0
32
+ Requires-Dist: huggingface-hub==1.1.7
33
+ Requires-Dist: datasets==4.4.1
34
34
  Dynamic: author
35
35
  Dynamic: author-email
36
36
  Dynamic: classifier
@@ -1,3 +1,4 @@
1
+ MANIFEST.in
1
2
  README.md
2
3
  __init__.py
3
4
  __main__.py
@@ -5,6 +6,8 @@ api.py
5
6
  atomgit_hub.py
6
7
  cli.py
7
8
  config.py
9
+ rate_limiter.py
10
+ requirements.txt
8
11
  setup.py
9
12
  test.py
10
13
  utils.py
@@ -14,6 +17,7 @@ utils.py
14
17
  ./atomgit_hub.py
15
18
  ./cli.py
16
19
  ./config.py
20
+ ./rate_limiter.py
17
21
  ./test.py
18
22
  ./utils.py
19
23
  atomgit.egg-info/PKG-INFO
@@ -21,4 +25,5 @@ atomgit.egg-info/SOURCES.txt
21
25
  atomgit.egg-info/dependency_links.txt
22
26
  atomgit.egg-info/entry_points.txt
23
27
  atomgit.egg-info/requires.txt
24
- atomgit.egg-info/top_level.txt
28
+ atomgit.egg-info/top_level.txt
29
+ tests/test_rate_limiter.py
@@ -3,5 +3,5 @@ requests>=2.31.0
3
3
  tqdm>=4.66.1
4
4
  colorama>=0.4.6
5
5
  tabulate>=0.9.0
6
- huggingface-hub>=0.20.0
7
- datasets>=2.16.0
6
+ huggingface-hub==1.1.7
7
+ datasets==4.4.1
@@ -14,6 +14,8 @@ from pathlib import Path
14
14
 
15
15
  # 设置Hugging Face Hub的API端点为AtomGit
16
16
  os.environ["HF_ENDPOINT"] = "https://hub.atomgit.com"
17
+ # 禁用Xet协议,避免 xet-write-token 请求
18
+ os.environ["HF_HUB_DISABLE_XET"] = "1"
17
19
  # 设置缓存目录
18
20
  cache_dir = os.path.expanduser("~/.cache/atomgit")
19
21
  os.makedirs(cache_dir, exist_ok=True)
@@ -21,6 +23,12 @@ os.environ["HF_HOME"] = cache_dir
21
23
 
22
24
  from huggingface_hub import snapshot_download as hf_snapshot_download
23
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
+ from rate_limiter import install_hf_rate_limiter
30
+
31
+ install_hf_rate_limiter()
24
32
 
25
33
 
26
34
  try:
@@ -9,6 +9,8 @@ from getpass import getpass
9
9
 
10
10
  # 设置Hugging Face Hub的API端点为AtomGit
11
11
  os.environ["HF_ENDPOINT"] = "https://hub.atomgit.com"
12
+ # 禁用Xet协议,避免 xet-write-token 请求
13
+ os.environ["HF_HUB_DISABLE_XET"] = "1"
12
14
  # 设置缓存目录
13
15
  cache_dir = os.path.expanduser("~/.cache/atomgit")
14
16
  os.makedirs(cache_dir, exist_ok=True)
@@ -37,7 +39,7 @@ except ImportError:
37
39
 
38
40
 
39
41
  @click.group()
40
- @click.version_option(version='1.0.4')
42
+ @click.version_option(version='1.0.6')
41
43
  def cli():
42
44
  """AtomGit CLI - 基于Transformers和Hugging Face Hub的AtomGit平台模型文件上传下载工具"""
43
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()
@@ -0,0 +1,7 @@
1
+ click>=8.1.7
2
+ requests>=2.31.0
3
+ tqdm>=4.66.1
4
+ colorama>=0.4.6
5
+ tabulate>=0.9.0
6
+ huggingface-hub==1.1.7
7
+ datasets==4.4.1
@@ -31,7 +31,7 @@ def read_requirements():
31
31
 
32
32
  setup(
33
33
  name='atomgit',
34
- version='1.0.4',
34
+ version='1.0.6',
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