durable_huggingface_hub 0.2.0 → 0.2.1
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +10 -0
- data/CONTRIBUTORS.md +8 -0
- data/README.md +1 -1
- data/huggingface_hub.gemspec +1 -0
- data/integration-tests/test_hf_hub_download.sh +33 -0
- data/lib/durable_huggingface_hub/authentication.rb +3 -10
- data/lib/durable_huggingface_hub/cli.rb +247 -0
- data/lib/durable_huggingface_hub/file_download.rb +16 -8
- data/lib/durable_huggingface_hub/hf_api.rb +16 -4
- data/lib/durable_huggingface_hub/types/dataset_info.rb +18 -0
- data/lib/durable_huggingface_hub/types/user.rb +13 -0
- data/lib/durable_huggingface_hub/utils/http.rb +4 -0
- data/lib/durable_huggingface_hub/version.rb +1 -1
- metadata +19 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9693f9faf5186aeabfc3df8e1f2c615ec208cf9b13abc8f6cfcdcd6c0a563241
|
|
4
|
+
data.tar.gz: 6331e9e4eb9226f8d48ea90e61915d8de1f1beea77d8e78205cda7491127efd7
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e343052c327d7ddc433ef77087d2e3b0745e35ccdddc46cd06fd33cc7123d7f33d262ef7dd757fde32ce7ae3aacf4e2ea56049be68a0b4c5d5d852b485353f92
|
|
7
|
+
data.tar.gz: afeb7ed0f145916d722e5057d09325d0727e9e5bcf611bfbe413d7158d351427d02e1f6aec0f7c9728dc4cdb5eec36227855e83257ce9a053b6029db28a2abfa
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
- `hf_hub_download` now follows 3xx redirects issued by HuggingFace Hub
|
|
12
|
+
resolve URLs by adding the `faraday-follow_redirects` middleware
|
|
13
|
+
(`Utils::HttpClient#build_connection`).
|
|
14
|
+
- Streaming downloads no longer produce files prefixed with a redirect body.
|
|
15
|
+
The temp file is now opened once with `"wb"` (truncate) instead of being
|
|
16
|
+
appended to per-chunk with `"ab"`, which also reduces filesystem overhead.
|
|
17
|
+
|
|
8
18
|
## [0.2.0] - 2025-01-24
|
|
9
19
|
|
|
10
20
|
### Added
|
data/CONTRIBUTORS.md
ADDED
data/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# HuggingFace Hub Ruby
|
|
2
2
|
|
|
3
|
-
A pure Ruby implementation of the HuggingFace Hub client library. This library provides a
|
|
3
|
+
A pure Ruby implementation of the HuggingFace Hub client library. This library provides a interface to HuggingFace Hub for downloading models, datasets, and managing repositories - with zero Python dependencies.
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
data/huggingface_hub.gemspec
CHANGED
|
@@ -38,6 +38,7 @@ Gem::Specification.new do |spec|
|
|
|
38
38
|
|
|
39
39
|
# Runtime dependencies
|
|
40
40
|
spec.add_dependency "faraday", "~> 2.0"
|
|
41
|
+
spec.add_dependency "faraday-follow_redirects", "~> 0.3"
|
|
41
42
|
spec.add_dependency "faraday-multipart", "~> 1.0"
|
|
42
43
|
spec.add_dependency "faraday-retry", "~> 2.0"
|
|
43
44
|
spec.add_dependency "dry-struct", "~> 1.6"
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Integration test reproducing the exact scenario from issue #1:
|
|
3
|
+
# hf_hub_download fails on every dataset/model file: redirects not followed
|
|
4
|
+
# and streaming concatenates redirect body.
|
|
5
|
+
#
|
|
6
|
+
# Requires network access to huggingface.co.
|
|
7
|
+
# Run from the repo root: bash integration-tests/test_hf_hub_download.sh
|
|
8
|
+
|
|
9
|
+
set -euo pipefail
|
|
10
|
+
|
|
11
|
+
REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
|
12
|
+
|
|
13
|
+
echo "=== hf_hub_download integration test (issue #1) ==="
|
|
14
|
+
|
|
15
|
+
ruby -I "$REPO_ROOT/lib" - <<'RUBY'
|
|
16
|
+
require "huggingface_hub"
|
|
17
|
+
|
|
18
|
+
path = DurableHuggingfaceHub::FileDownload.hf_hub_download(
|
|
19
|
+
repo_id: "Trelis/tiny-shakespeare",
|
|
20
|
+
filename: "input.txt",
|
|
21
|
+
repo_type: "dataset",
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
abort "FAIL: returned path does not exist: #{path}" unless File.exist?(path)
|
|
25
|
+
|
|
26
|
+
first_line = File.open(path, "rb", &:readline).chomp
|
|
27
|
+
if first_line.start_with?("Temporary Redirect")
|
|
28
|
+
abort "FAIL: file is corrupted — starts with redirect body:\n #{first_line}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
puts "PASS: downloaded to #{path}"
|
|
32
|
+
puts " first line: #{first_line[0, 80]}"
|
|
33
|
+
RUBY
|
|
@@ -54,15 +54,8 @@ module DurableHuggingfaceHub
|
|
|
54
54
|
# Update configuration
|
|
55
55
|
Configuration.instance.token = token
|
|
56
56
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
username = user_info.name || "unknown"
|
|
60
|
-
|
|
61
|
-
puts "Login successful!"
|
|
62
|
-
puts "Token: #{masked}"
|
|
63
|
-
puts "User: #{username}"
|
|
64
|
-
|
|
65
|
-
masked
|
|
57
|
+
# Return user info for CLI
|
|
58
|
+
{ name: user_info.name, type: user_info.type || "user" }
|
|
66
59
|
end
|
|
67
60
|
|
|
68
61
|
# Logs out of HuggingFace Hub by removing the stored token.
|
|
@@ -106,7 +99,7 @@ module DurableHuggingfaceHub
|
|
|
106
99
|
token = Utils::Auth.get_token!(token: token)
|
|
107
100
|
|
|
108
101
|
client = Utils::HttpClient.new(token: token)
|
|
109
|
-
response = client.get("/api/whoami")
|
|
102
|
+
response = client.get("/api/whoami-v2")
|
|
110
103
|
|
|
111
104
|
Types::User.from_hash(response.body)
|
|
112
105
|
end
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "optparse"
|
|
4
|
+
require "json"
|
|
5
|
+
|
|
6
|
+
module DurableHuggingfaceHub
|
|
7
|
+
# Command-line interface for the Hugging Face Hub
|
|
8
|
+
class CLI
|
|
9
|
+
def initialize(args)
|
|
10
|
+
@args = args
|
|
11
|
+
@command = nil
|
|
12
|
+
@options = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def run
|
|
16
|
+
parse_args
|
|
17
|
+
execute_command
|
|
18
|
+
rescue StandardError => e
|
|
19
|
+
abort "Error: #{e.message}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def parse_args
|
|
25
|
+
if @args.empty?
|
|
26
|
+
print_usage
|
|
27
|
+
exit 0
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
@command = @args.shift
|
|
31
|
+
|
|
32
|
+
OptionParser.new do |opts|
|
|
33
|
+
case @command
|
|
34
|
+
when "login"
|
|
35
|
+
parse_login_options(opts)
|
|
36
|
+
when "logout"
|
|
37
|
+
parse_logout_options(opts)
|
|
38
|
+
when "whoami"
|
|
39
|
+
parse_whoami_options(opts)
|
|
40
|
+
when "download"
|
|
41
|
+
parse_download_options(opts)
|
|
42
|
+
when "info"
|
|
43
|
+
parse_info_options(opts)
|
|
44
|
+
when "list"
|
|
45
|
+
parse_list_options(opts)
|
|
46
|
+
when "scan-cache"
|
|
47
|
+
parse_scan_cache_options(opts)
|
|
48
|
+
when "help", "--help", "-h"
|
|
49
|
+
print_usage
|
|
50
|
+
exit 0
|
|
51
|
+
else
|
|
52
|
+
puts "Unknown command: #{@command}"
|
|
53
|
+
print_usage
|
|
54
|
+
exit 1
|
|
55
|
+
end
|
|
56
|
+
end.parse!(@args)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def parse_login_options(opts)
|
|
60
|
+
opts.banner = "Usage: dhf login [options]"
|
|
61
|
+
opts.on("-t", "--token TOKEN", "HuggingFace API token") { |v| @options[:token] = v }
|
|
62
|
+
opts.on("--add-to-git-credential", "Add token to git credential store") { @options[:add_to_git_credential] = true }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def parse_logout_options(opts)
|
|
66
|
+
opts.banner = "Usage: dhf logout"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def parse_whoami_options(opts)
|
|
70
|
+
opts.banner = "Usage: dhf whoami [options]"
|
|
71
|
+
opts.on("-t", "--token TOKEN", "HuggingFace API token") { |v| @options[:token] = v }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def parse_download_options(opts)
|
|
75
|
+
opts.banner = "Usage: dhf download REPO_ID [options]"
|
|
76
|
+
opts.on("-f", "--filename FILENAME", "Specific file to download") { |v| @options[:filename] = v }
|
|
77
|
+
opts.on("-r", "--revision REVISION", "Git revision (branch, tag, or commit)") { |v| @options[:revision] = v }
|
|
78
|
+
opts.on("--repo-type TYPE", "Repository type (model, dataset, space)") { |v| @options[:repo_type] = v }
|
|
79
|
+
opts.on("--cache-dir DIR", "Path to cache directory") { |v| @options[:cache_dir] = v }
|
|
80
|
+
opts.on("--snapshot", "Download entire repository snapshot") { @options[:snapshot] = true }
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_info_options(opts)
|
|
84
|
+
opts.banner = "Usage: dhf info REPO_ID [options]"
|
|
85
|
+
opts.on("--repo-type TYPE", "Repository type (model, dataset, space)") { |v| @options[:repo_type] = v }
|
|
86
|
+
opts.on("-r", "--revision REVISION", "Git revision (branch, tag, or commit)") { |v| @options[:revision] = v }
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def parse_list_options(opts)
|
|
90
|
+
opts.banner = "Usage: dhf list TYPE [options]"
|
|
91
|
+
opts.on("--author AUTHOR", "Filter by author") { |v| @options[:author] = v }
|
|
92
|
+
opts.on("--search QUERY", "Search query") { |v| @options[:search] = v }
|
|
93
|
+
opts.on("--limit N", Integer, "Limit number of results") { |v| @options[:limit] = v }
|
|
94
|
+
opts.on("--full", "Return full information") { @options[:full] = true }
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_scan_cache_options(opts)
|
|
98
|
+
opts.banner = "Usage: dhf scan-cache [options]"
|
|
99
|
+
opts.on("--cache-dir DIR", "Path to cache directory") { |v| @options[:cache_dir] = v }
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def execute_command
|
|
103
|
+
case @command
|
|
104
|
+
when "login"
|
|
105
|
+
cmd_login
|
|
106
|
+
when "logout"
|
|
107
|
+
cmd_logout
|
|
108
|
+
when "whoami"
|
|
109
|
+
cmd_whoami
|
|
110
|
+
when "download"
|
|
111
|
+
cmd_download
|
|
112
|
+
when "info"
|
|
113
|
+
cmd_info
|
|
114
|
+
when "list"
|
|
115
|
+
cmd_list
|
|
116
|
+
when "scan-cache"
|
|
117
|
+
cmd_scan_cache
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def cmd_login
|
|
122
|
+
result = DurableHuggingfaceHub.login(
|
|
123
|
+
token: @options[:token],
|
|
124
|
+
add_to_git_credential: @options[:add_to_git_credential] || false
|
|
125
|
+
)
|
|
126
|
+
puts "Successfully logged in as: #{result[:name]} (#{result[:type]})"
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def cmd_logout
|
|
130
|
+
DurableHuggingfaceHub.logout
|
|
131
|
+
puts "Successfully logged out"
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def cmd_whoami
|
|
135
|
+
result = DurableHuggingfaceHub.whoami(token: @options[:token])
|
|
136
|
+
puts JSON.pretty_generate(result)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def cmd_download
|
|
140
|
+
repo_id = @args.shift
|
|
141
|
+
abort "Error: REPO_ID required" unless repo_id
|
|
142
|
+
|
|
143
|
+
if @options[:snapshot]
|
|
144
|
+
path = DurableHuggingfaceHub.snapshot_download(
|
|
145
|
+
repo_id: repo_id,
|
|
146
|
+
revision: @options[:revision],
|
|
147
|
+
repo_type: @options[:repo_type] || "model",
|
|
148
|
+
cache_dir: @options[:cache_dir]
|
|
149
|
+
)
|
|
150
|
+
else
|
|
151
|
+
filename = @options[:filename]
|
|
152
|
+
abort "Error: --filename required for single file download" unless filename
|
|
153
|
+
|
|
154
|
+
path = DurableHuggingfaceHub.hf_hub_download(
|
|
155
|
+
repo_id: repo_id,
|
|
156
|
+
filename: filename,
|
|
157
|
+
revision: @options[:revision],
|
|
158
|
+
repo_type: @options[:repo_type] || "model",
|
|
159
|
+
cache_dir: @options[:cache_dir]
|
|
160
|
+
)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
puts "Downloaded to: #{path}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def cmd_info
|
|
167
|
+
repo_id = @args.shift
|
|
168
|
+
abort "Error: REPO_ID required" unless repo_id
|
|
169
|
+
|
|
170
|
+
repo_type = @options[:repo_type] || "model"
|
|
171
|
+
info = DurableHuggingfaceHub.repo_info(
|
|
172
|
+
repo_id,
|
|
173
|
+
repo_type: repo_type,
|
|
174
|
+
revision: @options[:revision]
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
puts JSON.pretty_generate(info.to_h)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def cmd_list
|
|
181
|
+
type = @args.shift
|
|
182
|
+
abort "Error: TYPE required (models, datasets, or spaces)" unless type
|
|
183
|
+
|
|
184
|
+
results = case type
|
|
185
|
+
when "models"
|
|
186
|
+
DurableHuggingfaceHub.list_models(
|
|
187
|
+
author: @options[:author],
|
|
188
|
+
search: @options[:search],
|
|
189
|
+
limit: @options[:limit],
|
|
190
|
+
full: @options[:full] || false
|
|
191
|
+
)
|
|
192
|
+
when "datasets"
|
|
193
|
+
DurableHuggingfaceHub.list_datasets(
|
|
194
|
+
author: @options[:author],
|
|
195
|
+
search: @options[:search],
|
|
196
|
+
limit: @options[:limit],
|
|
197
|
+
full: @options[:full] || false
|
|
198
|
+
)
|
|
199
|
+
when "spaces"
|
|
200
|
+
DurableHuggingfaceHub.list_spaces(
|
|
201
|
+
author: @options[:author],
|
|
202
|
+
search: @options[:search],
|
|
203
|
+
limit: @options[:limit],
|
|
204
|
+
full: @options[:full] || false
|
|
205
|
+
)
|
|
206
|
+
else
|
|
207
|
+
abort "Error: Unknown type '#{type}'. Must be models, datasets, or spaces"
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
results.each do |item|
|
|
211
|
+
puts JSON.pretty_generate(item.to_h)
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def cmd_scan_cache
|
|
216
|
+
info = DurableHuggingfaceHub.scan_cache_dir(cache_dir: @options[:cache_dir])
|
|
217
|
+
puts JSON.pretty_generate(info.to_h)
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def print_usage
|
|
221
|
+
puts <<~USAGE
|
|
222
|
+
Usage: dhf COMMAND [options]
|
|
223
|
+
|
|
224
|
+
Commands:
|
|
225
|
+
login Login to HuggingFace Hub
|
|
226
|
+
logout Logout from HuggingFace Hub
|
|
227
|
+
whoami Display current user information
|
|
228
|
+
download REPO_ID Download a file or repository
|
|
229
|
+
info REPO_ID Get information about a repository
|
|
230
|
+
list TYPE List models, datasets, or spaces
|
|
231
|
+
scan-cache Scan the local cache directory
|
|
232
|
+
help Show this help message
|
|
233
|
+
|
|
234
|
+
Examples:
|
|
235
|
+
dhf login --token hf_...
|
|
236
|
+
dhf whoami
|
|
237
|
+
dhf download bert-base-uncased --filename config.json
|
|
238
|
+
dhf download bert-base-uncased --snapshot
|
|
239
|
+
dhf info openai/whisper-large-v3
|
|
240
|
+
dhf list models --author openai --limit 10
|
|
241
|
+
dhf scan-cache
|
|
242
|
+
|
|
243
|
+
Use 'dhf COMMAND --help' for more information on a specific command.
|
|
244
|
+
USAGE
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
end
|
|
@@ -463,8 +463,11 @@ module DurableHuggingfaceHub
|
|
|
463
463
|
end
|
|
464
464
|
end
|
|
465
465
|
|
|
466
|
+
# Use the redirect-resolved URL so the streaming GET never sees a 3xx.
|
|
467
|
+
download_url = metadata[:resolved_url] || url_path
|
|
468
|
+
|
|
466
469
|
# Download the file to blob storage
|
|
467
|
-
download_to_blob(client,
|
|
470
|
+
download_to_blob(client, download_url, blob_path, metadata, progress)
|
|
468
471
|
|
|
469
472
|
# Create snapshot symlink
|
|
470
473
|
ensure_snapshot_link(blob_path, snapshot_path)
|
|
@@ -483,12 +486,16 @@ module DurableHuggingfaceHub
|
|
|
483
486
|
def self.get_file_metadata(client, url_path)
|
|
484
487
|
response = client.head(url_path)
|
|
485
488
|
|
|
486
|
-
# Extract metadata from headers (response is now a Faraday::Response object)
|
|
487
489
|
headers = response.headers
|
|
490
|
+
# After following redirects, env[:url] holds the final resolved URL.
|
|
491
|
+
# We store it so the subsequent streaming GET can target it directly,
|
|
492
|
+
# bypassing the redirect entirely (on_data fires below middleware).
|
|
493
|
+
resolved_url = response.env[:url].to_s
|
|
488
494
|
{
|
|
489
495
|
etag: extract_etag(headers["etag"] || headers["x-linked-etag"]),
|
|
490
496
|
size: headers["x-linked-size"]&.to_i,
|
|
491
|
-
commit_hash: headers["x-repo-commit"]
|
|
497
|
+
commit_hash: headers["x-repo-commit"],
|
|
498
|
+
resolved_url: resolved_url
|
|
492
499
|
}
|
|
493
500
|
end
|
|
494
501
|
|
|
@@ -543,11 +550,12 @@ module DurableHuggingfaceHub
|
|
|
543
550
|
end
|
|
544
551
|
|
|
545
552
|
begin
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
553
|
+
File.open(temp_path, "wb") do |f|
|
|
554
|
+
client.request(:get, url_path) do |req|
|
|
555
|
+
req.options.on_data = proc do |chunk, _overall_received_bytes, _env|
|
|
556
|
+
f.write(chunk)
|
|
557
|
+
progress_tracker.update(chunk.bytesize)
|
|
558
|
+
end
|
|
551
559
|
end
|
|
552
560
|
end
|
|
553
561
|
|
|
@@ -629,12 +629,24 @@ module DurableHuggingfaceHub
|
|
|
629
629
|
DurableHuggingfaceHub::Utils::Validators.validate_repo_id(repo_id)
|
|
630
630
|
repo_type = DurableHuggingfaceHub::Utils::Validators.validate_repo_type(repo_type)
|
|
631
631
|
|
|
632
|
-
|
|
632
|
+
# Extract organization from repo_id if not provided
|
|
633
|
+
repo_parts = repo_id.split("/")
|
|
634
|
+
if organization.nil? && repo_parts.length == 2
|
|
635
|
+
# Get current username to check if it's a personal repo
|
|
636
|
+
current_user = whoami
|
|
637
|
+
potential_org = repo_parts[0]
|
|
638
|
+
# Only set organization if it's not the current user
|
|
639
|
+
organization = potential_org unless potential_org == current_user.name
|
|
640
|
+
end
|
|
641
|
+
repo_name = repo_parts.last
|
|
642
|
+
|
|
643
|
+
path = "/api/repos/create"
|
|
633
644
|
payload = {
|
|
634
|
-
name:
|
|
635
|
-
private: private
|
|
636
|
-
organization: organization
|
|
645
|
+
name: repo_name,
|
|
646
|
+
private: private
|
|
637
647
|
}
|
|
648
|
+
payload[:type] = repo_type if repo_type != "model"
|
|
649
|
+
payload[:organization] = organization if organization
|
|
638
650
|
|
|
639
651
|
response = http_client.post(path, body: payload, timeout: timeout)
|
|
640
652
|
body = response.body.is_a?(String) ? JSON.parse(response.body) : response.body
|
|
@@ -96,6 +96,24 @@ module DurableHuggingfaceHub
|
|
|
96
96
|
# @return [Integer, nil] Trending score
|
|
97
97
|
attribute :trending_score, Types::OptionalInteger.default(nil)
|
|
98
98
|
|
|
99
|
+
# Transform API response to filter out unknown keys
|
|
100
|
+
def self.from_hash(data)
|
|
101
|
+
transformed = data.dup
|
|
102
|
+
|
|
103
|
+
# Filter out unknown keys to avoid dry-struct errors
|
|
104
|
+
known_keys = [:id, :sha, :last_modified, :tags, :siblings, :private, :gated,
|
|
105
|
+
:disabled, :downloads, :likes, :author, :created_at, :card_data,
|
|
106
|
+
:description, :citation, :downloads_all_time, :paperswithcode_id,
|
|
107
|
+
:trending_score,
|
|
108
|
+
"id", "sha", "last_modified", "tags", "siblings", "private", "gated",
|
|
109
|
+
"disabled", "downloads", "likes", "author", "created_at", "card_data",
|
|
110
|
+
"description", "citation", "downloads_all_time", "paperswithcode_id",
|
|
111
|
+
"trending_score"]
|
|
112
|
+
transformed = transformed.select { |k, _| known_keys.include?(k) }
|
|
113
|
+
|
|
114
|
+
new(transformed)
|
|
115
|
+
end
|
|
116
|
+
|
|
99
117
|
# Returns the list of file names in the repository.
|
|
100
118
|
#
|
|
101
119
|
# @return [Array<String>] File names
|
|
@@ -25,6 +25,10 @@ module DurableHuggingfaceHub
|
|
|
25
25
|
# @return [String, nil] User type (e.g., "user")
|
|
26
26
|
attribute :type, Types::OptionalString.default(nil)
|
|
27
27
|
|
|
28
|
+
# @!attribute [r] id
|
|
29
|
+
# @return [String, nil] User ID
|
|
30
|
+
attribute :id, Types::OptionalString.default(nil)
|
|
31
|
+
|
|
28
32
|
# @!attribute [r] name
|
|
29
33
|
# @return [String] Username
|
|
30
34
|
attribute :name, Types::String
|
|
@@ -49,6 +53,10 @@ module DurableHuggingfaceHub
|
|
|
49
53
|
# @return [Array<Hash>, nil] Organizations the user belongs to
|
|
50
54
|
attribute :orgs, Types::Array.of(Types::Hash).optional.default(nil)
|
|
51
55
|
|
|
56
|
+
# @!attribute [r] auth
|
|
57
|
+
# @return [Hash, nil] Authentication information including token details
|
|
58
|
+
attribute :auth, Types::Hash.optional.default(nil)
|
|
59
|
+
|
|
52
60
|
# Transform isPro from API to is_pro
|
|
53
61
|
def self.from_hash(data)
|
|
54
62
|
transformed = data.dup
|
|
@@ -64,6 +72,11 @@ module DurableHuggingfaceHub
|
|
|
64
72
|
transformed[:avatar_url] = transformed.delete(:avatarUrl)
|
|
65
73
|
end
|
|
66
74
|
|
|
75
|
+
# Filter out unknown keys to avoid dry-struct errors
|
|
76
|
+
known_keys = [:type, :name, :fullname, :email, :avatar_url, :is_pro, :orgs,
|
|
77
|
+
"type", "name", "fullname", "email", "avatar_url", "is_pro", "orgs"]
|
|
78
|
+
transformed = transformed.select { |k, _| known_keys.include?(k) }
|
|
79
|
+
|
|
67
80
|
new(transformed)
|
|
68
81
|
end
|
|
69
82
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "json"
|
|
4
4
|
require "faraday"
|
|
5
|
+
require "faraday/follow_redirects"
|
|
5
6
|
require "faraday/retry"
|
|
6
7
|
|
|
7
8
|
require_relative "../configuration"
|
|
@@ -173,6 +174,9 @@ module DurableHuggingfaceHub
|
|
|
173
174
|
conn.response :logger, @logger, { headers: true, bodies: false }
|
|
174
175
|
end
|
|
175
176
|
|
|
177
|
+
# Follow 3xx redirects (HuggingFace Hub issues 307s for file resolve URLs)
|
|
178
|
+
conn.response :follow_redirects, limit: 5
|
|
179
|
+
|
|
176
180
|
# Retry middleware with exponential backoff
|
|
177
181
|
conn.request :retry,
|
|
178
182
|
max: 3,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: durable_huggingface_hub
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.2.
|
|
4
|
+
version: 0.2.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Berube
|
|
@@ -23,6 +23,20 @@ dependencies:
|
|
|
23
23
|
- - "~>"
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: faraday-follow_redirects
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0.3'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0.3'
|
|
26
40
|
- !ruby/object:Gem::Dependency
|
|
27
41
|
name: faraday-multipart
|
|
28
42
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -274,14 +288,17 @@ files:
|
|
|
274
288
|
- ".editorconfig"
|
|
275
289
|
- ".rubocop.yml"
|
|
276
290
|
- CHANGELOG.md
|
|
291
|
+
- CONTRIBUTORS.md
|
|
277
292
|
- README.md
|
|
278
293
|
- Rakefile
|
|
279
294
|
- devenv.lock
|
|
280
295
|
- devenv.nix
|
|
281
296
|
- devenv.yaml
|
|
282
297
|
- huggingface_hub.gemspec
|
|
298
|
+
- integration-tests/test_hf_hub_download.sh
|
|
283
299
|
- lib/durable_huggingface_hub/authentication.rb
|
|
284
300
|
- lib/durable_huggingface_hub/cache.rb
|
|
301
|
+
- lib/durable_huggingface_hub/cli.rb
|
|
285
302
|
- lib/durable_huggingface_hub/configuration.rb
|
|
286
303
|
- lib/durable_huggingface_hub/constants.rb
|
|
287
304
|
- lib/durable_huggingface_hub/errors.rb
|
|
@@ -328,7 +345,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
328
345
|
- !ruby/object:Gem::Version
|
|
329
346
|
version: '0'
|
|
330
347
|
requirements: []
|
|
331
|
-
rubygems_version: 3.
|
|
348
|
+
rubygems_version: 3.7.2
|
|
332
349
|
specification_version: 4
|
|
333
350
|
summary: Pure Ruby client for HuggingFace Hub
|
|
334
351
|
test_files: []
|