Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
586 changes: 586 additions & 0 deletions huggingface_hub/src/api/buckets.rs

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions huggingface_hub/src/api/mod.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod buckets;
pub mod commits;
pub mod files;
pub mod repo;
Expand Down
17 changes: 14 additions & 3 deletions huggingface_hub/src/bin/hfrs/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,9 @@ fn format_hf_error(err: &HFError) -> String {
HFError::RepoNotFound { repo_id } => {
format!("Repository '{repo_id}' not found. If the repo is private, make sure you are authenticated.")
},
HFError::BucketNotFound { bucket_name } => {
format!("Bucket '{bucket_name}' not found. If the bucket is private, make sure you are authenticated.")
},
HFError::EntryNotFound { path, repo_id } => {
format!("File '{path}' not found in repository '{repo_id}'.")
},
Expand All @@ -148,6 +151,17 @@ fn format_hf_error(err: &HFError) -> String {
HFError::AuthRequired => {
"Not authenticated. Run `hfrs auth login` or set the HF_TOKEN environment variable.".to_string()
},
HFError::Forbidden => {
"Permission denied. Check that your token has the required scopes for this operation.".to_string()
},
HFError::Conflict(body) => {
if body.contains("already exists") {
"Resource already exists. Use --exist-ok to skip this error.".to_string()
} else {
format!("Conflict: {body}")
}
},
HFError::RateLimited => "Rate limited. Please wait a moment and try again.".to_string(),
HFError::Http { status, url, body } => {
let status_code = status.as_u16();
match status_code {
Expand All @@ -158,9 +172,6 @@ fn format_hf_error(err: &HFError) -> String {
}
msg
},
403 => {
"Permission denied. Check that your token has the required scopes for this operation.".to_string()
},
404 => {
format!("Not found: {url}")
},
Expand Down
29 changes: 29 additions & 0 deletions huggingface_hub/src/blocking.rs
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,15 @@ pub struct HFSpaceSync {
pub(crate) inner: Arc<repo::HFSpace>,
}

/// Synchronous handle for Storage Bucket operations.
///
/// Obtain via [`HFClientSync::bucket`]. All methods block the current thread.
#[derive(Clone)]
pub struct HFBucketSync {
pub(crate) inner: crate::buckets::HFBucket,
pub(crate) runtime: Arc<tokio::runtime::Runtime>,
}

impl fmt::Debug for HFClientSync {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HFClientSync").finish()
Expand Down Expand Up @@ -120,6 +129,14 @@ impl HFClientSync {
pub fn space(&self, owner: impl Into<String>, name: impl Into<String>) -> HFSpaceSync {
HFSpaceSync::new(self.clone(), owner, name)
}

/// Creates a synchronous bucket handle.
pub fn bucket(&self, namespace: impl Into<String>, repo: impl Into<String>) -> HFBucketSync {
HFBucketSync {
inner: self.inner.bucket(namespace, repo),
runtime: self.runtime.clone(),
}
}
}

impl Deref for HFClientSync {
Expand Down Expand Up @@ -206,6 +223,18 @@ impl From<HFSpaceSync> for Arc<HFRepositorySync> {
/// Alias for [`HFRepositorySync`].
pub type HFRepoSync = HFRepositorySync;

#[cfg(test)]
mod bucket_tests {
#[test]
fn bucket_sync_constructor() {
use crate::HFClientBuilder;
let client = crate::blocking::HFClientSync::from_api(HFClientBuilder::new().build().unwrap()).unwrap();
let bucket = client.bucket("myuser", "my-bucket");
assert_eq!(bucket.inner.namespace, "myuser");
assert_eq!(bucket.inner.repo, "my-bucket");
}
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down
40 changes: 40 additions & 0 deletions huggingface_hub/src/buckets.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use crate::HFClient;

/// Handle for operations on a single HuggingFace Storage Bucket.
///
/// Obtain via [`HFClient::bucket`]. Every method adds `Authorization: Bearer <token>`
/// using the token configured on the client.
///
/// # Example
///
/// ```rust,no_run
/// # #[tokio::main]
/// # async fn main() -> huggingface_hub::Result<()> {
/// let client = huggingface_hub::HFClient::new()?;
/// let bucket = client.bucket("my-org", "my-bucket");
/// let overview = bucket.info().await?;
/// println!("Bucket size: {} bytes", overview.size);
/// # Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct HFBucket {
pub(crate) client: HFClient,
/// The namespace (user or organization) that owns the bucket.
pub namespace: String,
/// The bucket name within the namespace.
pub bucket: String,
}

impl HFClient {
/// Creates a handle for operations on a single Storage Bucket.
///
/// No I/O is performed — this simply binds the namespace and name to the client.
pub fn bucket(&self, namespace: impl Into<String>, repo: impl Into<String>) -> HFBucket {
HFBucket {
client: self.clone(),
namespace: namespace.into(),
bucket: repo.into(),
}
}
}
2 changes: 1 addition & 1 deletion huggingface_hub/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -293,7 +293,7 @@ impl HFClient {
revision,
repo_id: repo_id_str,
}),
crate::error::NotFoundContext::Generic => Err(HFError::Http { status, url, body }),
_ => Err(HFError::Http { status, url, body }),
},
_ => Err(HFError::Http { status, url, body }),
}
Expand Down
24 changes: 24 additions & 0 deletions huggingface_hub/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ pub enum HFError {
#[error("Repository not found: {repo_id}")]
RepoNotFound { repo_id: String },

#[error("Repository not found: {bucket_name}")]
BucketNotFound { bucket_name: String },

#[error("Revision not found: {revision} in {repo_id}")]
RevisionNotFound { repo_id: String, revision: String },

Expand Down Expand Up @@ -64,6 +67,25 @@ pub enum HFError {

#[error("{0}")]
Other(String),

#[error("forbidden")]
Forbidden,
#[error("conflict: {0}")]
Conflict(String),
#[error("rate limited")]
RateLimited,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn new_error_variants_display() {
assert_eq!(HFError::Forbidden.to_string(), "forbidden");
assert_eq!(HFError::Conflict("name taken".to_string()).to_string(), "conflict: name taken");
assert_eq!(HFError::RateLimited.to_string(), "rate limited");
}
}

impl HFError {
Expand Down Expand Up @@ -96,6 +118,8 @@ pub type Result<T> = std::result::Result<T, HFError>;
pub(crate) enum NotFoundContext {
/// 404 means the repository does not exist
Repo,
/// 404 means the bucket does not exist
Bucket,
/// 404 means a file/path does not exist within the repo
Entry { path: String },
/// 404 means the revision does not exist
Expand Down
4 changes: 3 additions & 1 deletion huggingface_hub/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ mod macros;
pub mod api;
#[cfg(feature = "blocking")]
pub mod blocking;
pub mod buckets;
pub mod cache;
pub mod client;
pub(crate) mod constants;
Expand All @@ -33,7 +34,8 @@ pub mod types;
pub mod xet;

#[cfg(feature = "blocking")]
pub use blocking::{HFClientSync, HFRepoSync, HFRepositorySync, HFSpaceSync};
pub use blocking::{HFBucketSync, HFClientSync, HFRepoSync, HFRepositorySync, HFSpaceSync};
pub use buckets::HFBucket;
pub use client::{HFClient, HFClientBuilder};
#[cfg(feature = "cli")]
#[doc(hidden)]
Expand Down
2 changes: 1 addition & 1 deletion huggingface_hub/src/pagination.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ impl HFClient {

/// Parse the `Link` header for a `rel="next"` URL.
/// Format: `<https://huggingface.co/api/models?p=1>; rel="next"`
fn parse_link_header_next(headers: &HeaderMap) -> Option<Url> {
pub(crate) fn parse_link_header_next(headers: &HeaderMap) -> Option<Url> {
let link_header = headers.get("link")?.to_str().ok()?;

for part in link_header.split(',') {
Expand Down
Loading
Loading