hfutils 0.1.2__tar.gz → 0.2.1__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 (51) hide show
  1. {hfutils-0.1.2/hfutils.egg-info → hfutils-0.2.1}/PKG-INFO +11 -2
  2. {hfutils-0.1.2 → hfutils-0.2.1}/README.md +8 -0
  3. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/config/meta.py +1 -1
  4. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/entry/cli.py +2 -0
  5. hfutils-0.2.1/hfutils/entry/ls.py +189 -0
  6. hfutils-0.2.1/hfutils/index/__init__.py +4 -0
  7. hfutils-0.2.1/hfutils/index/fetch.py +284 -0
  8. hfutils-0.2.1/hfutils/index/hash.py +22 -0
  9. hfutils-0.2.1/hfutils/index/make.py +215 -0
  10. hfutils-0.2.1/hfutils/index/validate.py +111 -0
  11. {hfutils-0.1.2 → hfutils-0.2.1/hfutils.egg-info}/PKG-INFO +11 -2
  12. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils.egg-info/SOURCES.txt +6 -0
  13. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils.egg-info/requires.txt +2 -1
  14. {hfutils-0.1.2 → hfutils-0.2.1}/requirements.txt +2 -1
  15. {hfutils-0.1.2 → hfutils-0.2.1}/LICENSE +0 -0
  16. {hfutils-0.1.2 → hfutils-0.2.1}/MANIFEST.in +0 -0
  17. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/__init__.py +0 -0
  18. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/archive/__init__.py +0 -0
  19. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/archive/base.py +0 -0
  20. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/archive/rar.py +0 -0
  21. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/archive/sevenz.py +0 -0
  22. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/archive/tar.py +0 -0
  23. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/archive/zip.py +0 -0
  24. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/config/__init__.py +0 -0
  25. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/entry/__init__.py +0 -0
  26. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/entry/base.py +0 -0
  27. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/entry/dispatch.py +0 -0
  28. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/entry/download.py +0 -0
  29. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/entry/upload.py +0 -0
  30. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/operate/__init__.py +0 -0
  31. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/operate/base.py +0 -0
  32. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/operate/download.py +0 -0
  33. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/operate/upload.py +0 -0
  34. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/operate/validate.py +0 -0
  35. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/utils/__init__.py +0 -0
  36. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/utils/binary.py +0 -0
  37. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/utils/download.py +0 -0
  38. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/utils/temp.py +0 -0
  39. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/utils/tqdm_.py +0 -0
  40. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils/utils/walk.py +0 -0
  41. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils.egg-info/dependency_links.txt +0 -0
  42. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils.egg-info/entry_points.txt +0 -0
  43. {hfutils-0.1.2 → hfutils-0.2.1}/hfutils.egg-info/top_level.txt +0 -0
  44. {hfutils-0.1.2 → hfutils-0.2.1}/requirements-7z.txt +0 -0
  45. {hfutils-0.1.2 → hfutils-0.2.1}/requirements-build.txt +0 -0
  46. {hfutils-0.1.2 → hfutils-0.2.1}/requirements-doc.txt +0 -0
  47. {hfutils-0.1.2 → hfutils-0.2.1}/requirements-rar.txt +0 -0
  48. {hfutils-0.1.2 → hfutils-0.2.1}/requirements-test.txt +0 -0
  49. {hfutils-0.1.2 → hfutils-0.2.1}/requirements-transfer.txt +0 -0
  50. {hfutils-0.1.2 → hfutils-0.2.1}/setup.cfg +0 -0
  51. {hfutils-0.1.2 → hfutils-0.2.1}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hfutils
3
- Version: 0.1.2
3
+ Version: 0.2.1
4
4
  Summary: Useful utilities for huggingface
5
5
  Home-page: https://github.com/deepghs/hfutils
6
6
  Author: narugo1992
@@ -21,10 +21,11 @@ Requires-Python: >=3.8
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: hbutils>=0.9.0
24
- Requires-Dist: huggingface_hub>=0.20
24
+ Requires-Dist: huggingface_hub>=0.22
25
25
  Requires-Dist: tqdm
26
26
  Requires-Dist: requests
27
27
  Requires-Dist: click>=7
28
+ Requires-Dist: tzlocal
28
29
  Provides-Extra: rar
29
30
  Requires-Dist: rarfile; extra == "rar"
30
31
  Provides-Extra: test
@@ -229,6 +230,14 @@ hfutils download -h
229
230
 
230
231
  ```
231
232
 
233
+ ## List Files in Repository
234
+
235
+ List files in repositories
236
+
237
+ ```shell
238
+ hfutils ls -r your/repository -o /your/local/file -d subdir/in/repo
239
+ ```
240
+
232
241
  ## Supported Formats
233
242
 
234
243
  By default, we support the `zip` and `tar` formats, including `.zip`, `.tar`, `.tar.gz`, `.tar.bz2`, and `.tar.xz`.
@@ -156,6 +156,14 @@ hfutils download -h
156
156
 
157
157
  ```
158
158
 
159
+ ## List Files in Repository
160
+
161
+ List files in repositories
162
+
163
+ ```shell
164
+ hfutils ls -r your/repository -o /your/local/file -d subdir/in/repo
165
+ ```
166
+
159
167
  ## Supported Formats
160
168
 
161
169
  By default, we support the `zip` and `tar` formats, including `.zip`, `.tar`, `.tar.gz`, `.tar.bz2`, and `.tar.xz`.
@@ -7,7 +7,7 @@ Overview:
7
7
  __TITLE__ = 'hfutils'
8
8
 
9
9
  #: Version of this project.
10
- __VERSION__ = '0.1.2'
10
+ __VERSION__ = '0.2.1'
11
11
 
12
12
  #: Short description of the project, will be included in ``setup.py``.
13
13
  __DESCRIPTION__ = 'Useful utilities for huggingface'
@@ -1,10 +1,12 @@
1
1
  from .dispatch import hfutilcli
2
2
  from .download import _add_download_subcommand
3
+ from .ls import _add_ls_subcommand
3
4
  from .upload import _add_upload_subcommand
4
5
 
5
6
  _DECORATORS = [
6
7
  _add_download_subcommand,
7
8
  _add_upload_subcommand,
9
+ _add_ls_subcommand,
8
10
  ]
9
11
 
10
12
  cli = hfutilcli
@@ -0,0 +1,189 @@
1
+ import mimetypes
2
+ import os.path
3
+ from enum import Enum, unique
4
+ from typing import Union, List
5
+
6
+ import click
7
+ import tzlocal
8
+ from huggingface_hub.hf_api import RepoFolder, RepoFile
9
+
10
+ from .base import CONTEXT_SETTINGS
11
+ from ..operate.base import REPO_TYPES, get_hf_client
12
+
13
+ mimetypes.add_type('image/webp', '.webp')
14
+
15
+
16
+ @unique
17
+ class ListItemType(Enum):
18
+ """
19
+ Enum class representing different types of list items.
20
+ """
21
+
22
+ FILE = 0x1
23
+ FOLDER = 0x2
24
+ IMAGE = 0x3
25
+ ARCHIVE = 0x4
26
+ MODEL = 0x5
27
+ DATA = 0x6
28
+
29
+ @property
30
+ def render_color(self):
31
+ """
32
+ Get the render color based on the item type.
33
+
34
+ :return: The render color for the item type.
35
+ :rtype: str
36
+ """
37
+ if self == self.FILE:
38
+ return None
39
+ elif self == self.FOLDER:
40
+ return 'blue'
41
+ elif self == self.IMAGE:
42
+ return 'magenta'
43
+ elif self == self.ARCHIVE:
44
+ return 'red'
45
+ elif self == self.MODEL:
46
+ return 'green'
47
+ elif self == self.DATA:
48
+ return 'yellow'
49
+ else:
50
+ raise ValueError(f'Unknown type - {self!r}') # pragma: no cover
51
+
52
+
53
+ class ListItem:
54
+ """
55
+ Class representing a list item.
56
+
57
+ :param item: The item object.
58
+ :type item: Union[RepoFolder, RepoFile]
59
+ :param base_dir: The base directory.
60
+ :type base_dir: str
61
+ """
62
+
63
+ def __init__(self, item: Union[RepoFolder, RepoFile], base_dir: str):
64
+ self.item = item
65
+ self.base_dir = base_dir
66
+ if isinstance(item, RepoFolder):
67
+ self.type = ListItemType.FOLDER
68
+ else:
69
+ mimetype, _ = mimetypes.guess_type(item.path)
70
+ _, ext = os.path.splitext(item.path)
71
+ self.type = ListItemType.FILE
72
+ if ext in {'.ckpt', '.pt', '.safetensors', '.onnx', '.model', '.h5', '.mlmodel',
73
+ '.ftz', '.pb', '.pth', '.tflite'}:
74
+ self.type = ListItemType.MODEL
75
+ elif ext in {'.json', '.csv', '.tsv', '.arrow', '.bin', '.msgpack', '.npy', '.npz',
76
+ '.parquet', '.pickle', '.pkl', '.wasm'}:
77
+ self.type = ListItemType.DATA
78
+ elif mimetype:
79
+ if 'image' in mimetype:
80
+ self.type = ListItemType.IMAGE
81
+ elif mimetype.split('/', maxsplit=1)[-1] in {'zip', 'rar', 'x-tar', 'x-7z-compressed'}:
82
+ self.type = ListItemType.ARCHIVE
83
+
84
+
85
+ def _add_ls_subcommand(cli: click.Group) -> click.Group:
86
+ """
87
+ Add the 'ls' subcommand to the CLI.
88
+
89
+ :param cli: The Click CLI application.
90
+ :type cli: click.Group
91
+ :return: The modified Click CLI application.
92
+ :rtype: click.Group
93
+ """
94
+
95
+ @cli.command('ls', help='List files from HuggingFace repository.\n\n'
96
+ 'Set environment $HF_TOKEN to use your own access token.',
97
+ context_settings=CONTEXT_SETTINGS)
98
+ @click.option('-r', '--repository', 'repo_id', type=str, required=True,
99
+ help='Repository to download from.')
100
+ @click.option('-t', '--type', 'repo_type', type=click.Choice(REPO_TYPES), default='dataset',
101
+ help='Type of the HuggingFace repository.', show_default=True)
102
+ @click.option('-d', '--directory', 'dir_in_repo', type=str, default=None,
103
+ help='Directory in repository to download the full directory tree.')
104
+ @click.option('-R', '--revision', 'revision', type=str, default='main',
105
+ help='Revision of repository.', show_default=True)
106
+ @click.option('-a', '--all', 'show_all', is_flag=True, type=bool, default=False,
107
+ help='Show all files, including hidden files.', show_default=True)
108
+ @click.option('-l', '--list', 'show_detailed', is_flag=True, type=bool, default=False,
109
+ help='Show detailed file information.', show_default=True)
110
+ def ls(repo_id: str, repo_type: str, dir_in_repo, revision: str, show_all: bool, show_detailed: bool):
111
+ """
112
+ List files from a HuggingFace repository.
113
+
114
+ :param repo_id: The identifier of the repository.
115
+ :type repo_id: str
116
+ :param repo_type: The type of the HuggingFace repository.
117
+ :type repo_type: str
118
+ :param dir_in_repo: The directory in the repository to list files from.
119
+ :type dir_in_repo: str
120
+ :param revision: The revision of the repository.
121
+ :type revision: str
122
+ :param show_all: Flag to indicate whether to show all files, including hidden files.
123
+ :type show_all: bool
124
+ :param show_detailed: Flag to indicate whether to show detailed file information.
125
+ :type show_detailed: bool
126
+ """
127
+ hf_client = get_hf_client()
128
+ items: List[ListItem] = []
129
+ for item in hf_client.list_repo_tree(
130
+ repo_id=repo_id,
131
+ repo_type=repo_type,
132
+ path_in_repo=dir_in_repo,
133
+ revision=revision,
134
+ recursive=False,
135
+ expand=show_detailed,
136
+ ):
137
+ relpath = os.path.relpath(item.path, dir_in_repo)
138
+ if not show_all and relpath.startswith('.'):
139
+ continue
140
+
141
+ item = ListItem(item, dir_in_repo)
142
+ items.append(item)
143
+ if not show_detailed:
144
+ print(click.style(
145
+ os.path.relpath(item.item.path, item.base_dir),
146
+ fg=item.type.render_color
147
+ ))
148
+
149
+ if show_detailed:
150
+ max_size_length = 0
151
+ max_commit_info_length = 0
152
+ for item in items:
153
+ if item.type == ListItemType.FOLDER:
154
+ size_text = '-'
155
+ else:
156
+ size_text = str(item.item.size)
157
+ max_size_length = max(max_size_length, len(size_text))
158
+
159
+ commit_text = (item.item.last_commit.title or '').splitlines(keepends=False)[0]
160
+ max_commit_info_length = max(max_commit_info_length, len(commit_text))
161
+
162
+ for item in items:
163
+ print('d' if item.type == ListItemType.FOLDER else '-', end='')
164
+ print('L' if item.type != ListItemType.FOLDER and item.item.lfs else '-', end='')
165
+
166
+ commit_text = (item.item.last_commit.title or '').splitlines(keepends=False)[0]
167
+ print(
168
+ ' ' + ' ' * (max_commit_info_length - len(commit_text)) +
169
+ click.style(commit_text, underline=True),
170
+ end=''
171
+ )
172
+
173
+ if item.type == ListItemType.FOLDER:
174
+ size_text = '-'
175
+ else:
176
+ size_text = str(item.item.size)
177
+ size_text = ' ' * (max_size_length - len(size_text)) + size_text
178
+ print(' ' + size_text, end='')
179
+
180
+ print(' ' + item.item.last_commit.oid[:8], end='')
181
+ commit_time = item.item.last_commit.date.astimezone(tzlocal.get_localzone())
182
+ print(' ' + commit_time.strftime('%Y-%m-%d %H:%M %Z'), end='')
183
+
184
+ print(' ' + click.style(
185
+ os.path.relpath(item.item.path, item.base_dir),
186
+ fg=item.type.render_color
187
+ ))
188
+
189
+ return cli
@@ -0,0 +1,4 @@
1
+ from .fetch import hf_tar_list_files, hf_tar_file_download, hf_tar_get_index, hf_tar_file_exists, \
2
+ ArchiveStandaloneFileIncompleteDownload, ArchiveStandaloneFileHashNotMatch
3
+ from .make import tar_create_index, hf_tar_create_index, tar_get_index_info, hf_tar_create_from_directory
4
+ from .validate import hf_tar_item_validate, hf_tar_validate
@@ -0,0 +1,284 @@
1
+ import json
2
+ import os.path
3
+ from typing import Optional, Dict, Union
4
+
5
+ from huggingface_hub.file_download import http_get, hf_hub_url
6
+ from huggingface_hub.utils import build_hf_headers
7
+
8
+ from .hash import _f_sha256
9
+ from ..operate.base import RepoTypeTyping, get_hf_client
10
+
11
+
12
+ class ArchiveStandaloneFileIncompleteDownload(Exception):
13
+ """
14
+ Exception raised when a standalone file in an archive is incompletely downloaded.
15
+ """
16
+
17
+
18
+ class ArchiveStandaloneFileHashNotMatch(Exception):
19
+ """
20
+ Exception raised when the hash of a standalone file in an archive does not match.
21
+ """
22
+
23
+
24
+ def hf_tar_get_index(repo_id: str, archive_in_repo: str,
25
+ repo_type: RepoTypeTyping = 'dataset', revision: str = 'main',
26
+ idx_repo_id: Optional[str] = None, idx_file_in_repo: Optional[str] = None,
27
+ idx_repo_type: Optional[RepoTypeTyping] = None, idx_revision: Optional[str] = None,
28
+ hf_token: Optional[str] = None):
29
+ """
30
+ Get the index of a tar archive file in a Hugging Face repository.
31
+
32
+ :param repo_id: The identifier of the repository.
33
+ :type repo_id: str
34
+ :param archive_in_repo: The path to the archive file in the repository.
35
+ :type archive_in_repo: str
36
+ :param repo_type: The type of the Hugging Face repository.
37
+ :type repo_type: RepoTypeTyping, optional
38
+ :param revision: The revision of the repository.
39
+ :type revision: str, optional
40
+ :param idx_repo_id: The identifier of the index repository.
41
+ :type idx_repo_id: str, optional
42
+ :param idx_file_in_repo: The path to the index file in the index repository.
43
+ :type idx_file_in_repo: str, optional
44
+ :param idx_repo_type: The type of the index repository.
45
+ :type idx_repo_type: RepoTypeTyping, optional
46
+ :param idx_revision: The revision of the index repository.
47
+ :type idx_revision: str, optional
48
+ :param hf_token: The Hugging Face access token.
49
+ :type hf_token: str, optional
50
+ :return: The index of the tar archive file.
51
+ :rtype: Dict
52
+ """
53
+ hf_client = get_hf_client(hf_token)
54
+ body, _ = os.path.splitext(archive_in_repo)
55
+ default_index_file = f'{body}.json'
56
+ with open(hf_client.hf_hub_download(
57
+ repo_id=idx_repo_id or repo_id,
58
+ repo_type=idx_repo_type or repo_type,
59
+ filename=idx_file_in_repo or default_index_file,
60
+ revision=idx_revision or revision,
61
+ ), 'r') as f:
62
+ return json.load(f)
63
+
64
+
65
+ def hf_tar_list_files(repo_id: str, archive_in_repo: str,
66
+ repo_type: RepoTypeTyping = 'dataset', revision: str = 'main',
67
+ idx_repo_id: Optional[str] = None, idx_file_in_repo: Optional[str] = None,
68
+ idx_repo_type: Optional[RepoTypeTyping] = None, idx_revision: Optional[str] = None,
69
+ hf_token: Optional[str] = None):
70
+ """
71
+ List files inside a tar archive file in a Hugging Face repository.
72
+
73
+ :param repo_id: The identifier of the repository.
74
+ :type repo_id: str
75
+ :param archive_in_repo: The path to the archive file in the repository.
76
+ :type archive_in_repo: str
77
+ :param repo_type: The type of the Hugging Face repository.
78
+ :type repo_type: RepoTypeTyping, optional
79
+ :param revision: The revision of the repository.
80
+ :type revision: str, optional
81
+ :param idx_repo_id: The identifier of the index repository.
82
+ :type idx_repo_id: str, optional
83
+ :param idx_file_in_repo: The path to the index file in the index repository.
84
+ :type idx_file_in_repo: str, optional
85
+ :param idx_repo_type: The type of the index repository.
86
+ :type idx_repo_type: RepoTypeTyping, optional
87
+ :param idx_revision: The revision of the index repository.
88
+ :type idx_revision: str, optional
89
+ :param hf_token: The Hugging Face access token.
90
+ :type hf_token: str, optional
91
+ :return: The list of files inside the tar archive.
92
+ :rtype: List[str]
93
+ """
94
+ index_data = hf_tar_get_index(
95
+ repo_id=repo_id,
96
+ archive_in_repo=archive_in_repo,
97
+ repo_type=repo_type,
98
+ revision=revision,
99
+
100
+ idx_repo_id=idx_repo_id,
101
+ idx_file_in_repo=idx_file_in_repo,
102
+ idx_repo_type=idx_repo_type,
103
+ idx_revision=idx_revision,
104
+
105
+ hf_token=hf_token,
106
+ )
107
+
108
+ return list(index_data['files'].keys())
109
+
110
+
111
+ def hf_tar_file_exists(repo_id: str, archive_in_repo: str, file_in_archive: str,
112
+ repo_type: RepoTypeTyping = 'dataset', revision: str = 'main',
113
+ idx_repo_id: Optional[str] = None, idx_file_in_repo: Optional[str] = None,
114
+ idx_repo_type: Optional[RepoTypeTyping] = None, idx_revision: Optional[str] = None,
115
+ hf_token: Optional[str] = None):
116
+ """
117
+ Check if a file exists inside a tar archive file in a Hugging Face repository.
118
+
119
+ :param repo_id: The identifier of the repository.
120
+ :type repo_id: str
121
+ :param archive_in_repo: The path to the archive file in the repository.
122
+ :type archive_in_repo: str
123
+ :param file_in_archive: The path to the file inside the archive.
124
+ :type file_in_archive: str
125
+ :param repo_type: The type of the Hugging Face repository.
126
+ :type repo_type: RepoTypeTyping, optional
127
+ :param revision: The revision of the repository.
128
+ :type revision: str, optional
129
+ :param idx_repo_id: The identifier of the index repository.
130
+ :type idx_repo_id: str, optional
131
+ :param idx_file_in_repo: The path to the index file in the index repository.
132
+ :type idx_file_in_repo: str, optional
133
+ :param idx_repo_type: The type of the index repository.
134
+ :type idx_repo_type: RepoTypeTyping, optional
135
+ :param idx_revision: The revision of the index repository.
136
+ :type idx_revision: str, optional
137
+ :param hf_token: The Hugging Face access token.
138
+ :type hf_token: str, optional
139
+ :return: True if the file exists, False otherwise.
140
+ :rtype: bool
141
+ """
142
+ index = hf_tar_get_index(
143
+ repo_id=repo_id,
144
+ archive_in_repo=archive_in_repo,
145
+ repo_type=repo_type,
146
+ revision=revision,
147
+
148
+ idx_repo_id=idx_repo_id,
149
+ idx_file_in_repo=idx_file_in_repo,
150
+ idx_repo_type=idx_repo_type,
151
+ idx_revision=idx_revision,
152
+
153
+ hf_token=hf_token,
154
+ )
155
+ files = _hf_files_process(index['files'])
156
+ return _n_path(file_in_archive) in files
157
+
158
+
159
+ def _n_path(path):
160
+ """
161
+ Normalize a file path.
162
+
163
+ :param path: The file path to normalize.
164
+ :type path: str
165
+ :return: The normalized file path.
166
+ :rtype: str
167
+ """
168
+ return os.path.normpath(os.path.join('/', path))
169
+
170
+
171
+ def _hf_files_process(files: Dict[str, dict]):
172
+ """
173
+ Normalize file paths in a dictionary of files.
174
+
175
+ :param files: The dictionary of files.
176
+ :type files: Dict[str, dict]
177
+ :return: The dictionary of files with normalized paths.
178
+ :rtype: Dict[str, dict]
179
+ """
180
+ return {_n_path(key): value for key, value in files.items()}
181
+
182
+
183
+ def hf_tar_file_download(repo_id: str, archive_in_repo: str, file_in_archive: str, local_file: str,
184
+ repo_type: RepoTypeTyping = 'dataset', revision: str = 'main',
185
+ idx_repo_id: Optional[str] = None, idx_file_in_repo: Optional[str] = None,
186
+ idx_repo_type: Optional[RepoTypeTyping] = None, idx_revision: Optional[str] = None,
187
+ proxies: Optional[Dict] = None, user_agent: Union[Dict, str, None] = None,
188
+ headers: Optional[Dict[str, str]] = None, endpoint: Optional[str] = None,
189
+ hf_token: Optional[str] = None):
190
+ """
191
+ Download a file from a tar archive file in a Hugging Face repository.
192
+
193
+ :param repo_id: The identifier of the repository.
194
+ :type repo_id: str
195
+ :param archive_in_repo: The path to the archive file in the repository.
196
+ :type archive_in_repo: str
197
+ :param file_in_archive: The path to the file inside the archive.
198
+ :type file_in_archive: str
199
+ :param local_file: The path to save the downloaded file locally.
200
+ :type local_file: str
201
+ :param repo_type: The type of the Hugging Face repository.
202
+ :type repo_type: RepoTypeTyping, optional
203
+ :param revision: The revision of the repository.
204
+ :type revision: str, optional
205
+ :param idx_repo_id: The identifier of the index repository.
206
+ :type idx_repo_id: str, optional
207
+ :param idx_file_in_repo: The path to the index file in the index repository.
208
+ :type idx_file_in_repo: str, optional
209
+ :param idx_repo_type: The type of the index repository.
210
+ :type idx_repo_type: RepoTypeTyping, optional
211
+ :param idx_revision: The revision of the index repository.
212
+ :type idx_revision: str, optional
213
+ :param proxies: The proxies to be used for the HTTP request.
214
+ :type proxies: Dict, optional
215
+ :param user_agent: The user agent for the HTTP request.
216
+ :type user_agent: Union[Dict, str, None], optional
217
+ :param headers: The additional headers for the HTTP request.
218
+ :type headers: Dict[str, str], optional
219
+ :param endpoint: The Hugging Face API endpoint.
220
+ :type endpoint: str, optional
221
+ :param hf_token: The Hugging Face access token.
222
+ :type hf_token: str, optional
223
+ """
224
+ index = hf_tar_get_index(
225
+ repo_id=repo_id,
226
+ archive_in_repo=archive_in_repo,
227
+ repo_type=repo_type,
228
+ revision=revision,
229
+
230
+ idx_repo_id=idx_repo_id,
231
+ idx_file_in_repo=idx_file_in_repo,
232
+ idx_repo_type=idx_repo_type,
233
+ idx_revision=idx_revision,
234
+
235
+ hf_token=hf_token,
236
+ )
237
+ files = _hf_files_process(index['files'])
238
+ if _n_path(file_in_archive) not in files:
239
+ raise FileNotFoundError(f'File {file_in_archive!r} not found '
240
+ f'in {repo_type}s/{repo_id}@{revision}/{archive_in_repo}.')
241
+
242
+ info = files[_n_path(file_in_archive)]
243
+ url_to_download = hf_hub_url(repo_id, archive_in_repo, repo_type=repo_type, revision=revision, endpoint=endpoint)
244
+ headers = build_hf_headers(
245
+ token=hf_token,
246
+ library_name=None,
247
+ library_version=None,
248
+ user_agent=user_agent,
249
+ headers=headers,
250
+ )
251
+ start_bytes = info['offset']
252
+ end_bytes = info['offset'] + info['size'] - 1
253
+ headers['Range'] = f'bytes={start_bytes}-{end_bytes}'
254
+
255
+ if os.path.dirname(local_file):
256
+ os.makedirs(os.path.dirname(local_file), exist_ok=True)
257
+ try:
258
+ with open(local_file, 'wb') as f:
259
+ http_get(
260
+ url_to_download,
261
+ f,
262
+ proxies=proxies,
263
+ resume_size=0,
264
+ headers=headers,
265
+ expected_size=info['size'],
266
+ displayed_filename=file_in_archive,
267
+ )
268
+
269
+ if os.path.getsize(local_file) != info['size']:
270
+ raise ArchiveStandaloneFileIncompleteDownload(
271
+ f'Expected size is {info["size"]}, but actually {os.path.getsize(local_file)} downloaded.'
272
+ )
273
+
274
+ if info.get('sha256'):
275
+ _sha256 = _f_sha256(local_file)
276
+ if _sha256 != info['sha256']:
277
+ raise ArchiveStandaloneFileHashNotMatch(
278
+ f'Expected hash is {info["sha256"]!r}, but actually {_sha256!r} found.'
279
+ )
280
+
281
+ except Exception:
282
+ if os.path.exists(local_file):
283
+ os.remove(local_file)
284
+ raise
@@ -0,0 +1,22 @@
1
+ from hashlib import sha256
2
+
3
+
4
+ def _f_sha256(file, chunk_for_hash: int = 1 << 20):
5
+ """
6
+ Calculate the SHA-256 hash of a file.
7
+
8
+ :param file: The path to the file.
9
+ :type file: str
10
+ :param chunk_for_hash: The chunk size for hashing, defaults to 1 << 20 (1 MB).
11
+ :type chunk_for_hash: int, optional
12
+ :return: The SHA-256 hash of the file.
13
+ :rtype: str
14
+ """
15
+ file_sha = sha256()
16
+ with open(file, 'rb') as f:
17
+ while True:
18
+ data = f.read(chunk_for_hash)
19
+ if not data:
20
+ break
21
+ file_sha.update(data)
22
+ return file_sha.hexdigest()
@@ -0,0 +1,215 @@
1
+ import json
2
+ import logging
3
+ import os
4
+ import tarfile
5
+ from hashlib import sha256, sha1
6
+ from typing import Optional
7
+
8
+ from .hash import _f_sha256
9
+ from ..archive import archive_pack
10
+ from ..operate import download_file_to_file, upload_file_to_file, upload_directory_as_directory
11
+ from ..operate.base import RepoTypeTyping
12
+ from ..utils import TemporaryDirectory, tqdm
13
+
14
+
15
+ def tar_get_index_info(src_tar_file, chunk_for_hash: int = 1 << 20, with_hash: bool = True, silent: bool = False):
16
+ """
17
+ Get the index information of a tar archive file.
18
+
19
+ :param src_tar_file: The path to the source tar archive file.
20
+ :type src_tar_file: str
21
+ :param chunk_for_hash: The chunk size for hashing, defaults to 1 << 20 (1 MB).
22
+ :type chunk_for_hash: int, optional
23
+ :param with_hash: Whether to include file hashes in the index, defaults to True.
24
+ :type with_hash: bool, optional
25
+ :param silent: Whether to suppress progress bars and logging messages, defaults to False.
26
+ :type silent: bool, optional
27
+ :return: The index information of the tar archive file.
28
+ :rtype: dict
29
+ """
30
+ filesize = os.path.getsize(src_tar_file)
31
+ sha_common = sha1()
32
+ sha_common.update(f'blob {filesize}\0'.encode('utf-8'))
33
+ sha_lfs = sha256()
34
+ logging.info(f'Calculating hash of {src_tar_file!r} ...')
35
+ with open(src_tar_file, 'rb') as f:
36
+ # make sure the big files will not cause OOM
37
+ while True:
38
+ data = f.read(chunk_for_hash)
39
+ if not data:
40
+ break
41
+ sha_common.update(data)
42
+ sha_lfs.update(data)
43
+
44
+ logging.info(f'Indexing tar file {src_tar_file!r} ...')
45
+ files = {}
46
+ with tarfile.open(src_tar_file, mode='r|') as tar:
47
+ for tarinfo in tqdm(tar, desc='Indexing tar file ...', silent=silent):
48
+ tarinfo: tarfile.TarInfo
49
+ if tarinfo.isreg():
50
+ info = {
51
+ 'offset': tarinfo.offset_data,
52
+ 'size': tarinfo.size,
53
+ }
54
+ if with_hash:
55
+ with TemporaryDirectory() as td:
56
+ tar.extract(tarinfo, td)
57
+ dst_file = os.path.join(td, tarinfo.name)
58
+ info['sha256'] = _f_sha256(dst_file)
59
+
60
+ files[tarinfo.name] = info
61
+
62
+ return {
63
+ 'filesize': filesize,
64
+ 'hash': sha_common.hexdigest(),
65
+ 'hash_lfs': sha_lfs.hexdigest(),
66
+ 'files': files,
67
+ }
68
+
69
+
70
+ def tar_create_index(src_tar_file, dst_index_file: Optional[str] = None,
71
+ chunk_for_hash: int = 1 << 20, with_hash: bool = True, silent: bool = False):
72
+ """
73
+ Create an index file for a tar archive file.
74
+
75
+ :param src_tar_file: The path to the source tar archive file.
76
+ :type src_tar_file: str
77
+ :param dst_index_file: The path to save the index file, defaults to None.
78
+ :type dst_index_file: str, optional
79
+ :param chunk_for_hash: The chunk size for hashing, defaults to 1 << 20 (1 MB).
80
+ :type chunk_for_hash: int, optional
81
+ :param with_hash: Whether to include file hashes in the index, defaults to True.
82
+ :type with_hash: bool, optional
83
+ :param silent: Whether to suppress progress bars and logging messages, defaults to False.
84
+ :type silent: bool, optional
85
+ :return: The path to the created index file.
86
+ :rtype: str
87
+ """
88
+ body, _ = os.path.splitext(src_tar_file)
89
+ dst_index_file = dst_index_file or f'{body}.json'
90
+ with open(dst_index_file, 'w') as f:
91
+ json.dump(tar_get_index_info(src_tar_file, chunk_for_hash, with_hash, silent), f)
92
+ return dst_index_file
93
+
94
+
95
+ def hf_tar_create_index(repo_id: str, archive_in_repo: str,
96
+ repo_type: RepoTypeTyping = 'dataset', revision: str = 'main',
97
+ idx_repo_id: Optional[str] = None, idx_file_in_repo: Optional[str] = None,
98
+ idx_repo_type: Optional[RepoTypeTyping] = None, idx_revision: Optional[str] = None,
99
+ chunk_for_hash: int = 1 << 20, with_hash: bool = True, skip_when_synced: bool = True,
100
+ hf_token: Optional[str] = None, ):
101
+ """
102
+ Create an index file for a tar archive file in a Hugging Face repository.
103
+
104
+ :param repo_id: The identifier of the repository.
105
+ :type repo_id: str
106
+ :param archive_in_repo: The path to the tar archive file.
107
+ :type archive_in_repo: str
108
+ :param repo_type: The type of the Hugging Face repository, defaults to 'dataset'.
109
+ :type repo_type: RepoTypeTyping, optional
110
+ :param revision: The revision of the repository, defaults to 'main'.
111
+ :type revision: str, optional
112
+ :param idx_repo_id: The identifier of the index repository, defaults to None.
113
+ :type idx_repo_id: str, optional
114
+ :param idx_file_in_repo: The path to save the index file in the index repository, defaults to None.
115
+ :type idx_file_in_repo: str, optional
116
+ :param idx_repo_type: The type of the index repository, defaults to None.
117
+ :type idx_repo_type: RepoTypeTyping, optional
118
+ :param idx_revision: The revision of the index repository, defaults to None.
119
+ :type idx_revision: str, optional
120
+ :param chunk_for_hash: The chunk size for hashing, defaults to 1 << 20 (1 MB).
121
+ :type chunk_for_hash: int, optional
122
+ :param with_hash: Whether to include file hashes in the index, defaults to True.
123
+ :type with_hash: bool, optional
124
+ :param skip_when_synced: Skip syncing when index is ready, defaults to True.
125
+ :type skip_when_synced: bool
126
+ :param hf_token: The Hugging Face access token, defaults to None.
127
+ :type hf_token: str, optional
128
+ """
129
+ body, _ = os.path.splitext(archive_in_repo)
130
+ default_index_filename = f'{body}.json'
131
+
132
+ from .validate import hf_tar_validate
133
+ if skip_when_synced and hf_tar_validate(
134
+ repo_id=repo_id,
135
+ repo_type=repo_type,
136
+ archive_in_repo=archive_in_repo,
137
+ revision=revision,
138
+
139
+ idx_repo_id=idx_repo_id or repo_id,
140
+ idx_repo_type=idx_repo_type or repo_type,
141
+ idx_file_in_repo=idx_file_in_repo or default_index_filename,
142
+ idx_revision=idx_revision or revision,
143
+
144
+ hf_token=hf_token,
145
+ ):
146
+ logging.info(f'Entry {repo_type}s/{repo_id}/{archive_in_repo} already indexed, skipped.')
147
+ return
148
+
149
+ with TemporaryDirectory() as td:
150
+ local_tar_file = os.path.join(td, os.path.basename(archive_in_repo))
151
+ download_file_to_file(
152
+ repo_id=repo_id,
153
+ repo_type=repo_type,
154
+ file_in_repo=archive_in_repo,
155
+ local_file=local_tar_file,
156
+ revision=revision,
157
+ hf_token=hf_token,
158
+ )
159
+ dst_index_file = tar_create_index(local_tar_file, chunk_for_hash=chunk_for_hash, with_hash=with_hash)
160
+
161
+ upload_file_to_file(
162
+ repo_id=idx_repo_id or repo_id,
163
+ repo_type=idx_repo_type or repo_type,
164
+ file_in_repo=idx_file_in_repo or default_index_filename,
165
+ local_file=dst_index_file,
166
+ revision=idx_revision or revision,
167
+ hf_token=hf_token,
168
+ message=f'Create index for {repo_type}s/{repo_id}@{revision}/{archive_in_repo}',
169
+ )
170
+
171
+
172
+ def hf_tar_create_from_directory(
173
+ repo_id: str, archive_in_repo: str, local_directory: str,
174
+ repo_type: RepoTypeTyping = 'dataset', revision: str = 'main',
175
+ chunk_for_hash: int = 1 << 20, with_hash: bool = True,
176
+ silent: bool = False, hf_token: Optional[str] = None):
177
+ """
178
+ Create a tar archive file from a local directory and upload it to a Hugging Face repository.
179
+
180
+ :param repo_id: The identifier of the repository.
181
+ :type repo_id: str
182
+ :param archive_in_repo: The path to save the tar archive file in the repository.
183
+ :type archive_in_repo: str
184
+ :param local_directory: The path to the local directory to be archived.
185
+ :type local_directory: str
186
+ :param repo_type: The type of the Hugging Face repository, defaults to 'dataset'.
187
+ :type repo_type: RepoTypeTyping, optional
188
+ :param revision: The revision of the repository, defaults to 'main'.
189
+ :type revision: str, optional
190
+ :param chunk_for_hash: The chunk size for hashing, defaults to 1 << 20 (1 MB).
191
+ :type chunk_for_hash: int, optional
192
+ :param with_hash: Whether to include file hashes in the index, defaults to True.
193
+ :type with_hash: bool, optional
194
+ :param silent: Whether to suppress progress bars and logging messages, defaults to False.
195
+ :type silent: bool, optional
196
+ :param hf_token: The Hugging Face access token, defaults to None.
197
+ :type hf_token: str, optional
198
+ """
199
+ _, ext = os.path.splitext(archive_in_repo)
200
+ with TemporaryDirectory() as td:
201
+ local_tar_file = os.path.join(td, archive_in_repo)
202
+ if os.path.dirname(local_tar_file):
203
+ os.makedirs(os.path.dirname(local_tar_file), exist_ok=True)
204
+ archive_pack('tar', local_directory, local_tar_file, silent=silent)
205
+ tar_create_index(local_tar_file, chunk_for_hash=chunk_for_hash, with_hash=with_hash, silent=silent)
206
+
207
+ upload_directory_as_directory(
208
+ repo_id=repo_id,
209
+ repo_type=repo_type,
210
+ path_in_repo='.',
211
+ local_directory=td,
212
+ revision=revision,
213
+ hf_token=hf_token,
214
+ message=f'Create indexed tar {repo_type}s/{repo_id}@{revision}/{archive_in_repo}'
215
+ )
@@ -0,0 +1,111 @@
1
+ from typing import Optional
2
+
3
+ from huggingface_hub.hf_api import RepoFile
4
+ from huggingface_hub.utils import EntryNotFoundError, RepositoryNotFoundError
5
+
6
+ from .fetch import hf_tar_get_index
7
+ from ..operate.base import RepoTypeTyping, get_hf_client
8
+
9
+
10
+ def hf_tar_item_validate(file_item: RepoFile, size: int, hash_: Optional[str] = None, hash_lfs: Optional[str] = None):
11
+ """
12
+ Validate a file item in a tar archive.
13
+
14
+ This function checks if the file item matches the expected size and hash.
15
+
16
+ :param file_item: The file item from the Hugging Face repository.
17
+ :type file_item: RepoFile
18
+ :param size: The expected size of the file.
19
+ :type size: int
20
+ :param hash_: The expected SHA-1 hash of the file.
21
+ :type hash_: str, optional
22
+ :param hash_lfs: The expected SHA-256 hash of the file if stored in LFS.
23
+ :type hash_lfs: str, optional
24
+ :return: True if the file item is valid, False otherwise.
25
+ :rtype: bool
26
+ """
27
+ # size not match
28
+ if (file_item.lfs and size != file_item.lfs.size) or \
29
+ (not file_item.lfs and size != file_item.size):
30
+ return False
31
+
32
+ # compare tar file hash
33
+ item_hashes = [file_item.blob_id]
34
+ if file_item.lfs:
35
+ item_hashes.append(file_item.lfs.sha256)
36
+ item_hashes = set(filter(bool, item_hashes))
37
+ cmp_hashes = [hash_, hash_lfs]
38
+ cmp_hashes = set(filter(bool, cmp_hashes))
39
+ return bool(cmp_hashes & item_hashes)
40
+
41
+
42
+ def hf_tar_validate(repo_id: str, archive_in_repo: str, repo_type: RepoTypeTyping = 'dataset', revision: str = 'main',
43
+ idx_repo_id: Optional[str] = None, idx_file_in_repo: Optional[str] = None,
44
+ idx_repo_type: Optional[RepoTypeTyping] = None, idx_revision: Optional[str] = None,
45
+ hf_token: Optional[str] = None):
46
+ """
47
+ Validate a tar archive in a Hugging Face repository.
48
+
49
+ This function validates if the tar archive in the Hugging Face repository matches the expected size and hash.
50
+
51
+ :param repo_id: The ID of the Hugging Face repository.
52
+ :type repo_id: str
53
+ :param archive_in_repo: The path to the tar archive in the repository.
54
+ :type archive_in_repo: str
55
+ :param repo_type: The type of the Hugging Face repository, defaults to 'dataset'.
56
+ :type repo_type: RepoTypeTyping, optional
57
+ :param revision: The revision of the repository, defaults to 'main'.
58
+ :type revision: str, optional
59
+ :param idx_repo_id: The ID of the repository where the index file is stored.
60
+ :type idx_repo_id: Optional[str], optional
61
+ :param idx_file_in_repo: The path to the index file in the repository.
62
+ :type idx_file_in_repo: Optional[str], optional
63
+ :param idx_repo_type: The type of the repository where the index file is stored.
64
+ :type idx_repo_type: Optional[RepoTypeTyping], optional
65
+ :param idx_revision: The revision of the repository where the index file is stored.
66
+ :type idx_revision: Optional[str], optional
67
+ :param hf_token: The Hugging Face token for authentication, defaults to None.
68
+ :type hf_token: Optional[str], optional
69
+ :raises EntryNotFoundError: If the specified entry is not found in the repository.
70
+ :raises IsADirectoryError: If the specified entry is a directory.
71
+ :return: True if the tar archive is valid, False otherwise.
72
+ :rtype: bool
73
+ """
74
+ hf_client = get_hf_client(hf_token)
75
+
76
+ items = list(hf_client.get_paths_info(
77
+ repo_id=repo_id,
78
+ repo_type=repo_type,
79
+ paths=[archive_in_repo],
80
+ revision=revision,
81
+ ))
82
+ if len(items) == 0:
83
+ raise EntryNotFoundError(f'Entry {repo_type}s/{repo_id}/{archive_in_repo} not found.')
84
+ elif not isinstance(items[0], RepoFile):
85
+ raise IsADirectoryError(f'Entry {repo_type}s/{repo_id}/{archive_in_repo} is a directory, not a file.')
86
+ else:
87
+ item = items[0]
88
+
89
+ try:
90
+ index = hf_tar_get_index(
91
+ repo_id=repo_id,
92
+ archive_in_repo=archive_in_repo,
93
+ repo_type=repo_type,
94
+ revision=revision,
95
+
96
+ idx_repo_id=idx_repo_id,
97
+ idx_file_in_repo=idx_file_in_repo,
98
+ idx_repo_type=idx_repo_type,
99
+ idx_revision=idx_revision,
100
+
101
+ hf_token=hf_token,
102
+ )
103
+ except (EntryNotFoundError, RepositoryNotFoundError):
104
+ return False
105
+
106
+ return hf_tar_item_validate(
107
+ file_item=item,
108
+ size=index['filesize'],
109
+ hash_=index.get('hash'),
110
+ hash_lfs=index.get('hash_lfs'),
111
+ )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: hfutils
3
- Version: 0.1.2
3
+ Version: 0.2.1
4
4
  Summary: Useful utilities for huggingface
5
5
  Home-page: https://github.com/deepghs/hfutils
6
6
  Author: narugo1992
@@ -21,10 +21,11 @@ Requires-Python: >=3.8
21
21
  Description-Content-Type: text/markdown
22
22
  License-File: LICENSE
23
23
  Requires-Dist: hbutils>=0.9.0
24
- Requires-Dist: huggingface_hub>=0.20
24
+ Requires-Dist: huggingface_hub>=0.22
25
25
  Requires-Dist: tqdm
26
26
  Requires-Dist: requests
27
27
  Requires-Dist: click>=7
28
+ Requires-Dist: tzlocal
28
29
  Provides-Extra: rar
29
30
  Requires-Dist: rarfile; extra == "rar"
30
31
  Provides-Extra: test
@@ -229,6 +230,14 @@ hfutils download -h
229
230
 
230
231
  ```
231
232
 
233
+ ## List Files in Repository
234
+
235
+ List files in repositories
236
+
237
+ ```shell
238
+ hfutils ls -r your/repository -o /your/local/file -d subdir/in/repo
239
+ ```
240
+
232
241
  ## Supported Formats
233
242
 
234
243
  By default, we support the `zip` and `tar` formats, including `.zip`, `.tar`, `.tar.gz`, `.tar.bz2`, and `.tar.xz`.
@@ -29,7 +29,13 @@ hfutils/entry/base.py
29
29
  hfutils/entry/cli.py
30
30
  hfutils/entry/dispatch.py
31
31
  hfutils/entry/download.py
32
+ hfutils/entry/ls.py
32
33
  hfutils/entry/upload.py
34
+ hfutils/index/__init__.py
35
+ hfutils/index/fetch.py
36
+ hfutils/index/hash.py
37
+ hfutils/index/make.py
38
+ hfutils/index/validate.py
33
39
  hfutils/operate/__init__.py
34
40
  hfutils/operate/base.py
35
41
  hfutils/operate/download.py
@@ -1,8 +1,9 @@
1
1
  hbutils>=0.9.0
2
- huggingface_hub>=0.20
2
+ huggingface_hub>=0.22
3
3
  tqdm
4
4
  requests
5
5
  click>=7
6
+ tzlocal
6
7
 
7
8
  [7z]
8
9
  py7zr
@@ -1,5 +1,6 @@
1
1
  hbutils>=0.9.0
2
- huggingface_hub>=0.20
2
+ huggingface_hub>=0.22
3
3
  tqdm
4
4
  requests
5
5
  click>=7
6
+ tzlocal
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes