durable_huggingface_hub 0.2.0

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 (35) hide show
  1. checksums.yaml +7 -0
  2. data/.editorconfig +29 -0
  3. data/.rubocop.yml +108 -0
  4. data/CHANGELOG.md +127 -0
  5. data/README.md +547 -0
  6. data/Rakefile +106 -0
  7. data/devenv.lock +171 -0
  8. data/devenv.nix +15 -0
  9. data/devenv.yaml +8 -0
  10. data/huggingface_hub.gemspec +63 -0
  11. data/lib/durable_huggingface_hub/authentication.rb +245 -0
  12. data/lib/durable_huggingface_hub/cache.rb +508 -0
  13. data/lib/durable_huggingface_hub/configuration.rb +191 -0
  14. data/lib/durable_huggingface_hub/constants.rb +145 -0
  15. data/lib/durable_huggingface_hub/errors.rb +412 -0
  16. data/lib/durable_huggingface_hub/file_download.rb +831 -0
  17. data/lib/durable_huggingface_hub/hf_api.rb +1278 -0
  18. data/lib/durable_huggingface_hub/repo_card.rb +430 -0
  19. data/lib/durable_huggingface_hub/types/cache_info.rb +298 -0
  20. data/lib/durable_huggingface_hub/types/commit_info.rb +149 -0
  21. data/lib/durable_huggingface_hub/types/dataset_info.rb +158 -0
  22. data/lib/durable_huggingface_hub/types/model_info.rb +154 -0
  23. data/lib/durable_huggingface_hub/types/space_info.rb +158 -0
  24. data/lib/durable_huggingface_hub/types/user.rb +179 -0
  25. data/lib/durable_huggingface_hub/types.rb +205 -0
  26. data/lib/durable_huggingface_hub/utils/auth.rb +174 -0
  27. data/lib/durable_huggingface_hub/utils/headers.rb +220 -0
  28. data/lib/durable_huggingface_hub/utils/http.rb +329 -0
  29. data/lib/durable_huggingface_hub/utils/paths.rb +230 -0
  30. data/lib/durable_huggingface_hub/utils/progress.rb +217 -0
  31. data/lib/durable_huggingface_hub/utils/retry.rb +165 -0
  32. data/lib/durable_huggingface_hub/utils/validators.rb +236 -0
  33. data/lib/durable_huggingface_hub/version.rb +8 -0
  34. data/lib/huggingface_hub.rb +205 -0
  35. metadata +334 -0
data/devenv.lock ADDED
@@ -0,0 +1,171 @@
1
+ {
2
+ "nodes": {
3
+ "devenv": {
4
+ "locked": {
5
+ "dir": "src/modules",
6
+ "lastModified": 1761091275,
7
+ "owner": "cachix",
8
+ "repo": "devenv",
9
+ "rev": "a795c32dc826b51d12706f27fb344f966bb2b084",
10
+ "type": "github"
11
+ },
12
+ "original": {
13
+ "dir": "src/modules",
14
+ "owner": "cachix",
15
+ "repo": "devenv",
16
+ "type": "github"
17
+ }
18
+ },
19
+ "flake-compat": {
20
+ "flake": false,
21
+ "locked": {
22
+ "lastModified": 1747046372,
23
+ "owner": "edolstra",
24
+ "repo": "flake-compat",
25
+ "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
26
+ "type": "github"
27
+ },
28
+ "original": {
29
+ "owner": "edolstra",
30
+ "repo": "flake-compat",
31
+ "type": "github"
32
+ }
33
+ },
34
+ "flake-compat_2": {
35
+ "flake": false,
36
+ "locked": {
37
+ "lastModified": 1747046372,
38
+ "owner": "edolstra",
39
+ "repo": "flake-compat",
40
+ "rev": "9100a0f413b0c601e0533d1d94ffd501ce2e7885",
41
+ "type": "github"
42
+ },
43
+ "original": {
44
+ "owner": "edolstra",
45
+ "repo": "flake-compat",
46
+ "type": "github"
47
+ }
48
+ },
49
+ "flake-utils": {
50
+ "inputs": {
51
+ "systems": "systems"
52
+ },
53
+ "locked": {
54
+ "lastModified": 1731533236,
55
+ "owner": "numtide",
56
+ "repo": "flake-utils",
57
+ "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
58
+ "type": "github"
59
+ },
60
+ "original": {
61
+ "owner": "numtide",
62
+ "repo": "flake-utils",
63
+ "type": "github"
64
+ }
65
+ },
66
+ "git-hooks": {
67
+ "inputs": {
68
+ "flake-compat": "flake-compat",
69
+ "gitignore": "gitignore",
70
+ "nixpkgs": [
71
+ "nixpkgs"
72
+ ]
73
+ },
74
+ "locked": {
75
+ "lastModified": 1760663237,
76
+ "owner": "cachix",
77
+ "repo": "git-hooks.nix",
78
+ "rev": "ca5b894d3e3e151ffc1db040b6ce4dcc75d31c37",
79
+ "type": "github"
80
+ },
81
+ "original": {
82
+ "owner": "cachix",
83
+ "repo": "git-hooks.nix",
84
+ "type": "github"
85
+ }
86
+ },
87
+ "gitignore": {
88
+ "inputs": {
89
+ "nixpkgs": [
90
+ "git-hooks",
91
+ "nixpkgs"
92
+ ]
93
+ },
94
+ "locked": {
95
+ "lastModified": 1709087332,
96
+ "owner": "hercules-ci",
97
+ "repo": "gitignore.nix",
98
+ "rev": "637db329424fd7e46cf4185293b9cc8c88c95394",
99
+ "type": "github"
100
+ },
101
+ "original": {
102
+ "owner": "hercules-ci",
103
+ "repo": "gitignore.nix",
104
+ "type": "github"
105
+ }
106
+ },
107
+ "nixpkgs": {
108
+ "locked": {
109
+ "lastModified": 1758532697,
110
+ "owner": "cachix",
111
+ "repo": "devenv-nixpkgs",
112
+ "rev": "207a4cb0e1253c7658c6736becc6eb9cace1f25f",
113
+ "type": "github"
114
+ },
115
+ "original": {
116
+ "owner": "cachix",
117
+ "ref": "rolling",
118
+ "repo": "devenv-nixpkgs",
119
+ "type": "github"
120
+ }
121
+ },
122
+ "nixpkgs-ruby": {
123
+ "inputs": {
124
+ "flake-compat": "flake-compat_2",
125
+ "flake-utils": "flake-utils",
126
+ "nixpkgs": [
127
+ "nixpkgs"
128
+ ]
129
+ },
130
+ "locked": {
131
+ "lastModified": 1759902829,
132
+ "owner": "bobvanderlinden",
133
+ "repo": "nixpkgs-ruby",
134
+ "rev": "5fba6c022a63f1e76dee4da71edddad8959f088a",
135
+ "type": "github"
136
+ },
137
+ "original": {
138
+ "owner": "bobvanderlinden",
139
+ "repo": "nixpkgs-ruby",
140
+ "type": "github"
141
+ }
142
+ },
143
+ "root": {
144
+ "inputs": {
145
+ "devenv": "devenv",
146
+ "git-hooks": "git-hooks",
147
+ "nixpkgs": "nixpkgs",
148
+ "nixpkgs-ruby": "nixpkgs-ruby",
149
+ "pre-commit-hooks": [
150
+ "git-hooks"
151
+ ]
152
+ }
153
+ },
154
+ "systems": {
155
+ "locked": {
156
+ "lastModified": 1681028828,
157
+ "owner": "nix-systems",
158
+ "repo": "default",
159
+ "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
160
+ "type": "github"
161
+ },
162
+ "original": {
163
+ "owner": "nix-systems",
164
+ "repo": "default",
165
+ "type": "github"
166
+ }
167
+ }
168
+ },
169
+ "root": "root",
170
+ "version": 7
171
+ }
data/devenv.nix ADDED
@@ -0,0 +1,15 @@
1
+ { pkgs, lib, config, inputs, ... }:
2
+
3
+ {
4
+ # https://devenv.sh/packages/
5
+ packages = with pkgs; [ git libyaml openssl ];
6
+
7
+ languages.ruby.enable = true;
8
+ languages.ruby.version = "3.4.7";
9
+
10
+
11
+ enterShell = ''
12
+
13
+ '';
14
+
15
+ }
data/devenv.yaml ADDED
@@ -0,0 +1,8 @@
1
+ inputs:
2
+ nixpkgs:
3
+ url: github:cachix/devenv-nixpkgs/rolling
4
+ nixpkgs-ruby:
5
+ url: github:bobvanderlinden/nixpkgs-ruby
6
+ inputs:
7
+ nixpkgs:
8
+ follows: nixpkgs
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "lib/durable_huggingface_hub/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "durable_huggingface_hub"
7
+ spec.version = DurableHuggingfaceHub::VERSION.to_s
8
+ spec.authors = ["David Berube"]
9
+ spec.email = ["commercial@durableprogramming.com"]
10
+
11
+ spec.summary = "Pure Ruby client for HuggingFace Hub"
12
+ spec.description = <<~DESC
13
+ A complete, production-ready Ruby implementation of the HuggingFace Hub client library.
14
+ Download models, datasets, and manage repositories with zero Python dependencies.
15
+ Features smart caching, authentication, progress tracking, and comprehensive error handling.
16
+ DESC
17
+ spec.homepage = "https://github.com/durableprogramming/huggingface-hub-ruby"
18
+ spec.license = "MIT"
19
+ spec.required_ruby_version = ">= 3.0.0"
20
+
21
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
22
+ spec.metadata["homepage_uri"] = spec.homepage
23
+ spec.metadata["source_code_uri"] = "https://github.com/durableprogramming/huggingface-hub-ruby"
24
+ spec.metadata["changelog_uri"] = "https://github.com/durableprogramming/huggingface-hub-ruby/blob/master/CHANGELOG.md"
25
+ spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/huggingface_hub"
26
+ spec.metadata["bug_tracker_uri"] = "https://github.com/durableprogramming/huggingface-hub-ruby/issues"
27
+
28
+ # Specify which files should be added to the gem when it is released.
29
+ spec.files = Dir.chdir(__dir__) do
30
+ `git ls-files -z`.split("\x0").reject do |f|
31
+ (File.expand_path(f) == __FILE__) ||
32
+ f.start_with?(*%w[bin/ test/ spec/ features/ .git .github appveyor Gemfile reference/ philosophy/ durableprogramming-coding-standards/])
33
+ end
34
+ end
35
+ spec.bindir = "exe"
36
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ["lib"]
38
+
39
+ # Runtime dependencies
40
+ spec.add_dependency "faraday", "~> 2.0"
41
+ spec.add_dependency "faraday-multipart", "~> 1.0"
42
+ spec.add_dependency "faraday-retry", "~> 2.0"
43
+ spec.add_dependency "dry-struct", "~> 1.6"
44
+ spec.add_dependency "dry-types", "~> 1.7"
45
+ spec.add_dependency "ruby-progressbar", "~> 1.13"
46
+ spec.add_dependency "zeitwerk", "~> 2.6"
47
+
48
+ # Development dependencies
49
+ spec.add_development_dependency "bundler", "~> 2.0"
50
+ spec.add_development_dependency "rake", "~> 13.0"
51
+ spec.add_development_dependency "minitest", "~> 5.0"
52
+ spec.add_development_dependency "minitest-reporters", "~> 1.6"
53
+ spec.add_development_dependency "webmock", "~> 3.18"
54
+ spec.add_development_dependency "vcr", "~> 6.1"
55
+ spec.add_development_dependency "rubocop", "~> 1.50"
56
+ spec.add_development_dependency "rubocop-minitest", "~> 0.31"
57
+ spec.add_development_dependency "rubocop-rake", "~> 0.6"
58
+ spec.add_development_dependency "yard", "~> 0.9"
59
+ spec.add_development_dependency "simplecov", "~> 0.22"
60
+
61
+ # For more information and examples about making a new gem, check out our
62
+ # guide at: https://bundler.io/guides/creating_gem.html
63
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "io/console"
4
+
5
+ module DurableHuggingfaceHub
6
+ # Authentication methods for HuggingFace Hub.
7
+ #
8
+ # This module provides methods for logging in and out of HuggingFace Hub,
9
+ # storing authentication tokens securely, and verifying credentials.
10
+ module Authentication
11
+ # Logs in to HuggingFace Hub and stores the authentication token.
12
+ #
13
+ # If a token is provided, it will be validated and stored. If no token
14
+ # is provided and the process is running interactively, the user will
15
+ # be prompted to enter a token.
16
+ #
17
+ # @param token [String, nil] Authentication token
18
+ # @param add_to_git_credential [Boolean] Whether to add token to git credential helper
19
+ # @return [String] The stored token (masked)
20
+ # @raise [DurableHuggingfaceHubError] If token is invalid or login fails
21
+ #
22
+ # @example With explicit token
23
+ # DurableHuggingfaceHub.login(token: "hf_...")
24
+ #
25
+ # @example Interactive login
26
+ # DurableHuggingfaceHub.login # Prompts for token
27
+ #
28
+ # @example Login with git credential storage
29
+ # DurableHuggingfaceHub.login(token: "hf_...", add_to_git_credential: true)
30
+ def self.login(token: nil, add_to_git_credential: false)
31
+ # Get token from parameter or prompt
32
+ token = obtain_token(token)
33
+
34
+ # Validate token format
35
+ unless Utils::Auth.valid_token_format?(token)
36
+ raise ValidationError.new("token", "Invalid token format. Token should start with 'hf_'")
37
+ end
38
+
39
+ # Verify token works by calling whoami
40
+ begin
41
+ user_info = whoami(token: token)
42
+ rescue HfHubHTTPError => e
43
+ raise DurableHuggingfaceHubError.new("Login failed: #{e.message}")
44
+ end
45
+
46
+ # Store token
47
+ Utils::Auth.write_token_to_file(token)
48
+
49
+ # Store in git credential helper if requested
50
+ if add_to_git_credential
51
+ store_git_credential(token)
52
+ end
53
+
54
+ # Update configuration
55
+ Configuration.instance.token = token
56
+
57
+ # Return masked token and user info
58
+ masked = Utils::Auth.mask_token(token)
59
+ username = user_info.name || "unknown"
60
+
61
+ puts "Login successful!"
62
+ puts "Token: #{masked}"
63
+ puts "User: #{username}"
64
+
65
+ masked
66
+ end
67
+
68
+ # Logs out of HuggingFace Hub by removing the stored token.
69
+ #
70
+ # @return [Boolean] True if token was removed, false if no token existed
71
+ #
72
+ # @example
73
+ # DurableHuggingfaceHub.logout
74
+ def self.logout
75
+ result = Utils::Auth.delete_token_file
76
+
77
+ # Remove from git credential helper if present
78
+ remove_git_credential
79
+
80
+ # Clear from configuration
81
+ Configuration.instance.token = nil
82
+
83
+ if result
84
+ puts "Logged out successfully. Token removed."
85
+ else
86
+ puts "No token found to remove."
87
+ end
88
+
89
+ result
90
+ end
91
+
92
+ # Returns information about the currently authenticated user.
93
+ #
94
+ # Makes a request to the /api/whoami endpoint to retrieve user information.
95
+ #
96
+ # @param token [String, nil] Authentication token (uses stored token if not provided)
97
+ # @return [Hash] User information hash
98
+ # @raise [LocalTokenNotFoundError] If no token is available
99
+ # @raise [HfHubHTTPError] If the API request fails
100
+ #
101
+ # @example
102
+ # user_info = DurableHuggingfaceHub.whoami
103
+ # puts user_info["name"]
104
+ # puts user_info["fullname"]
105
+ def self.whoami(token: nil)
106
+ token = Utils::Auth.get_token!(token: token)
107
+
108
+ client = Utils::HttpClient.new(token: token)
109
+ response = client.get("/api/whoami")
110
+
111
+ Types::User.from_hash(response.body)
112
+ end
113
+
114
+ # Checks if a user is currently logged in (has a valid token).
115
+ #
116
+ # @return [Boolean] True if a token is available
117
+ #
118
+ # @example
119
+ # if DurableHuggingfaceHub.logged_in?
120
+ # puts "Already logged in"
121
+ # end
122
+ def self.logged_in?
123
+ token = Utils::Auth.get_token
124
+ !token.nil? && !token.empty?
125
+ end
126
+
127
+ private
128
+
129
+ # Checks if git is available on the system.
130
+ #
131
+ # @return [Boolean] True if git is available
132
+ def self.git_available?
133
+ system("which git > /dev/null 2>&1")
134
+ end
135
+
136
+ # Stores the token in the git credential helper.
137
+ #
138
+ # @param token [String] Authentication token
139
+ # @return [Boolean] True if stored successfully
140
+ def self.store_git_credential(token)
141
+ # Check if git is available
142
+ return false unless git_available?
143
+
144
+ credential_data = <<~CREDENTIAL
145
+ protocol=https
146
+ host=huggingface.co
147
+ username=hf_#{token[3..]} # Extract token part after 'hf_'
148
+ password=#{token}
149
+ CREDENTIAL
150
+
151
+ # Use git credential approve to store the credential
152
+ begin
153
+ IO.popen("git credential approve", "w+") do |io|
154
+ io.write(credential_data)
155
+ io.close_write
156
+ io.read # Consume any output
157
+ end
158
+ $?.success?
159
+ rescue Errno::ENOENT, IOError
160
+ false
161
+ end
162
+ end
163
+
164
+ # Removes the token from the git credential helper.
165
+ #
166
+ # @return [Boolean] True if removed successfully
167
+ def self.remove_git_credential
168
+ # Check if git is available
169
+ return false unless git_available?
170
+
171
+ # First check if we have a stored token to know what to remove
172
+ token = Utils::Auth.get_token
173
+ return false unless token
174
+
175
+ credential_data = <<~CREDENTIAL
176
+ protocol=https
177
+ host=huggingface.co
178
+ username=hf_#{token[3..]} # Extract token part after 'hf_'
179
+ CREDENTIAL
180
+
181
+ # Use git credential reject to remove the credential
182
+ begin
183
+ IO.popen("git credential reject", "w+") do |io|
184
+ io.write(credential_data)
185
+ io.close_write
186
+ io.read # Consume any output
187
+ end
188
+ $?.success?
189
+ rescue Errno::ENOENT, IOError
190
+ false
191
+ end
192
+ end
193
+
194
+ # Obtains a token either from parameter or by prompting the user.
195
+ #
196
+ # @param token [String, nil] Provided token
197
+ # @return [String] Token
198
+ # @raise [DurableHuggingfaceHubError] If unable to obtain token
199
+ def self.obtain_token(token)
200
+ return token if token && !token.empty?
201
+
202
+ # Check if running interactively
203
+ unless $stdin.respond_to?(:tty?) && $stdin.tty?
204
+ raise DurableHuggingfaceHubError.new(
205
+ "No token provided and not running interactively. " \
206
+ "Please provide a token using the 'token' parameter."
207
+ )
208
+ end
209
+
210
+ # Prompt for token
211
+ prompt_for_token
212
+ end
213
+
214
+ # Prompts the user to enter their authentication token.
215
+ #
216
+ # The token input is hidden for security.
217
+ #
218
+ # @return [String] Entered token
219
+ # @raise [DurableHuggingfaceHubError] If token entry is cancelled
220
+ def self.prompt_for_token
221
+ puts "\nTo login, you need a User Access Token from https://huggingface.co/settings/tokens"
222
+ puts "You can create a new token or use an existing one."
223
+ puts "\nPaste your token (input will be hidden):"
224
+
225
+ # Read token without echoing to terminal
226
+ begin
227
+ if $stdin.respond_to?(:noecho) && $stdin.isatty
228
+ token = $stdin.noecho(&:gets)&.chomp
229
+ else
230
+ # For testing or when noecho is not available
231
+ token = $stdin.gets&.chomp
232
+ end
233
+ rescue Interrupt
234
+ puts "\nLogin cancelled"
235
+ raise DurableHuggingfaceHubError.new("Login cancelled by user")
236
+ end
237
+
238
+ if token.nil? || token.empty?
239
+ raise DurableHuggingfaceHubError.new("Token entry cancelled or empty")
240
+ end
241
+
242
+ token
243
+ end
244
+ end
245
+ end