pub mod commits;
pub mod diff;
pub mod download;
pub mod files;
pub mod listing;
pub mod repo_type;
pub mod upload;
use std::collections::HashMap;
use std::str::FromStr;
use bon::bon;
pub use commits::{CommitAuthor, DiffEntry, GitCommitInfo, GitRefInfo, GitRefs};
pub use diff::{GitStatus, HFDiffParseError, HFFileDiff};
pub use files::{
AddSource, BlobLfsInfo, BlobSecurityInfo, CommitInfo, CommitOperation, FileMetadataInfo, LastCommitInfo,
RepoTreeEntry, SourceByteStream, StreamFactory, StreamSource,
};
#[cfg(not(target_family = "wasm"))]
pub(crate) use files::{extract_file_size, extract_xet_hash};
use futures::Stream;
pub use repo_type::{RepoType, RepoTypeAny, RepoTypeDataset, RepoTypeKernel, RepoTypeModel, RepoTypeSpace};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize, Serializer};
use url::Url;
use crate::client::HFClient;
use crate::error::{HFError, HFResult};
use crate::{constants, retry};
#[derive(Debug, Clone)]
pub enum GatedApprovalMode {
Disabled,
Auto,
Manual,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum GatedNotificationsMode {
Bulk,
RealTime,
}
#[derive(Debug, Clone)]
pub struct GatedNotifications {
pub mode: GatedNotificationsMode,
pub email: Option<String>,
}
impl GatedNotifications {
pub fn new(mode: GatedNotificationsMode) -> Self {
Self { mode, email: None }
}
pub fn with_email(mut self, email: impl Into<String>) -> Self {
self.email = Some(email.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RepoSibling {
pub rfilename: String,
pub size: Option<u64>,
pub lfs: Option<BlobLfsInfo>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct SafeTensorsInfo {
pub parameters: HashMap<String, u64>,
pub total: u64,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "snake_case")]
pub struct TransformersInfo {
pub auto_model: String,
#[serde(default)]
pub custom_class: Option<String>,
#[serde(default)]
pub pipeline_tag: Option<String>,
#[serde(default)]
pub processor: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InferenceProviderMapping {
pub provider: String,
pub provider_id: String,
pub status: String,
pub task: String,
#[serde(default)]
pub adapter: Option<String>,
#[serde(default)]
pub adapter_weights_path: Option<String>,
#[serde(default, rename = "type")]
pub r#type: Option<String>,
}
fn deserialize_inference_provider_mapping<'de, D>(
deserializer: D,
) -> Result<Option<Vec<InferenceProviderMapping>>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
use serde::de::Error;
let value = Option::<serde_json::Value>::deserialize(deserializer)?;
let Some(value) = value else { return Ok(None) };
match value {
serde_json::Value::Null => Ok(None),
serde_json::Value::Array(items) => {
let mut out = Vec::with_capacity(items.len());
for item in items {
out.push(serde_json::from_value(item).map_err(D::Error::custom)?);
}
Ok(Some(out))
},
serde_json::Value::Object(map) => {
let mut out = Vec::with_capacity(map.len());
for (provider, mut value) in map {
if let serde_json::Value::Object(ref mut obj) = value {
obj.insert("provider".to_string(), serde_json::Value::String(provider));
}
out.push(serde_json::from_value(value).map_err(D::Error::custom)?);
}
Ok(Some(out))
},
_ => Err(D::Error::custom("expected list or object for inferenceProviderMapping")),
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EvalResultEntry {
pub dataset: EvalResultDataset,
pub value: serde_json::Value,
#[serde(default, rename = "verifyToken")]
pub verify_token: Option<String>,
#[serde(default)]
pub date: Option<String>,
#[serde(default)]
pub source: Option<EvalResultSource>,
#[serde(default)]
pub notes: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EvalResultDataset {
pub id: String,
pub task_id: String,
#[serde(default)]
pub revision: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct EvalResultSource {
#[serde(default)]
pub url: Option<String>,
#[serde(default)]
pub name: Option<String>,
#[serde(default)]
pub user: Option<String>,
#[serde(default)]
pub org: Option<String>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ModelInfo {
pub id: String,
#[serde(rename = "_id")]
pub internal_id: Option<String>,
pub author: Option<String>,
#[serde(default)]
pub base_models: Option<Vec<String>>,
pub card_data: Option<serde_json::Value>,
pub children_model_count: Option<u64>,
pub config: Option<serde_json::Value>,
pub created_at: Option<String>,
pub disabled: Option<bool>,
pub downloads: Option<u64>,
pub downloads_all_time: Option<u64>,
#[serde(default, rename = "evalResults")]
pub eval_results: Option<Vec<EvalResultEntry>>,
pub gated: Option<serde_json::Value>,
pub gguf: Option<serde_json::Value>,
pub inference: Option<String>,
#[serde(default, deserialize_with = "deserialize_inference_provider_mapping")]
pub inference_provider_mapping: Option<Vec<InferenceProviderMapping>>,
pub last_modified: Option<String>,
#[serde(rename = "library_name")]
pub library_name: Option<String>,
pub likes: Option<u64>,
#[serde(rename = "mask_token")]
pub mask_token: Option<String>,
#[serde(rename = "model-index")]
pub model_index: Option<serde_json::Value>,
#[serde(rename = "pipeline_tag")]
pub pipeline_tag: Option<String>,
pub private: Option<bool>,
pub resource_group: Option<serde_json::Value>,
pub safetensors: Option<SafeTensorsInfo>,
pub security_repo_status: Option<serde_json::Value>,
pub sha: Option<String>,
pub siblings: Option<Vec<RepoSibling>>,
pub spaces: Option<Vec<String>>,
pub tags: Option<Vec<String>>,
pub transformers_info: Option<TransformersInfo>,
pub trending_score: Option<f64>,
pub used_storage: Option<u64>,
pub widget_data: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DatasetInfo {
pub id: String,
#[serde(rename = "_id")]
pub internal_id: Option<String>,
pub author: Option<String>,
pub sha: Option<String>,
pub private: Option<bool>,
pub gated: Option<serde_json::Value>,
pub disabled: Option<bool>,
pub downloads: Option<u64>,
pub downloads_all_time: Option<u64>,
pub likes: Option<u64>,
pub tags: Option<Vec<String>>,
pub created_at: Option<String>,
pub last_modified: Option<String>,
pub siblings: Option<Vec<RepoSibling>>,
pub card_data: Option<serde_json::Value>,
pub citation: Option<String>,
#[serde(rename = "paperswithcode_id")]
pub paperswithcode_id: Option<String>,
pub resource_group: Option<serde_json::Value>,
pub trending_score: Option<f64>,
pub description: Option<String>,
pub used_storage: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SpaceInfo {
pub id: String,
#[serde(rename = "_id")]
pub internal_id: Option<String>,
pub author: Option<String>,
pub sha: Option<String>,
pub private: Option<bool>,
pub gated: Option<serde_json::Value>,
pub disabled: Option<bool>,
pub likes: Option<u64>,
pub tags: Option<Vec<String>>,
pub created_at: Option<String>,
pub last_modified: Option<String>,
pub siblings: Option<Vec<RepoSibling>>,
pub card_data: Option<serde_json::Value>,
pub sdk: Option<String>,
pub trending_score: Option<f64>,
pub host: Option<String>,
pub subdomain: Option<String>,
pub runtime: Option<crate::spaces::SpaceRuntime>,
pub datasets: Option<Vec<String>>,
pub models: Option<Vec<String>>,
pub resource_group: Option<serde_json::Value>,
pub used_storage: Option<u64>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct KernelInfo {
pub id: String,
#[serde(rename = "_id")]
pub internal_id: Option<String>,
pub author: Option<String>,
pub sha: Option<String>,
pub private: Option<bool>,
pub gated: Option<serde_json::Value>,
pub downloads: Option<u64>,
pub likes: Option<u64>,
pub last_modified: Option<String>,
pub trusted_publisher: Option<bool>,
pub supported_driver_families: Option<Vec<String>>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct RepoUrl {
pub url: String,
}
pub struct HFRepository<T: RepoType> {
pub(crate) hf_client: HFClient,
pub(super) owner: String,
pub(super) name: String,
pub(super) repo_type: T,
}
impl<T: RepoType> Clone for HFRepository<T> {
fn clone(&self) -> Self {
Self {
hf_client: self.hf_client.clone(),
owner: self.owner.clone(),
name: self.name.clone(),
repo_type: self.repo_type,
}
}
}
impl Serialize for GatedApprovalMode {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match self {
GatedApprovalMode::Disabled => serializer.serialize_bool(false),
GatedApprovalMode::Auto => serializer.serialize_str("auto"),
GatedApprovalMode::Manual => serializer.serialize_str("manual"),
}
}
}
impl FromStr for GatedApprovalMode {
type Err = HFError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"false" | "disabled" => Ok(GatedApprovalMode::Disabled),
"auto" => Ok(GatedApprovalMode::Auto),
"manual" => Ok(GatedApprovalMode::Manual),
_ => Err(HFError::InvalidParameter(format!(
"unknown gated approval mode: {s:?}. Expected 'auto', 'manual', or 'false'"
))),
}
}
}
impl FromStr for GatedNotificationsMode {
type Err = HFError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"bulk" => Ok(GatedNotificationsMode::Bulk),
"real-time" | "realtime" => Ok(GatedNotificationsMode::RealTime),
_ => Err(HFError::InvalidParameter(format!(
"unknown gated notifications mode: {s:?}. Expected 'bulk' or 'real-time'"
))),
}
}
}
impl<T: RepoType> std::fmt::Debug for HFRepository<T> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HFRepository")
.field("owner", &self.owner)
.field("name", &self.name)
.field("repo_type", &self.repo_type.singular())
.finish()
}
}
impl HFClient {
pub fn repository<T: RepoType>(
&self,
repo_type: T,
owner: impl Into<String>,
name: impl Into<String>,
) -> HFRepository<T> {
HFRepository::new(self.clone(), repo_type, owner, name)
}
pub fn model(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeModel> {
self.repository(RepoTypeModel, owner, name)
}
pub fn dataset(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeDataset> {
self.repository(RepoTypeDataset, owner, name)
}
pub fn space(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeSpace> {
self.repository(RepoTypeSpace, owner, name)
}
pub fn kernel(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeKernel> {
self.repository(RepoTypeKernel, owner, name)
}
}
#[bon]
impl HFClient {
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_models(
&self,
#[builder(into)]
search: Option<String>,
#[builder(into)]
author: Option<String>,
#[builder(into)]
filter: Option<String>,
#[builder(into)]
sort: Option<String>,
#[builder(into)]
pipeline_tag: Option<String>,
full: Option<bool>,
card_data: Option<bool>,
fetch_config: Option<bool>,
limit: Option<usize>,
) -> HFResult<impl Stream<Item = HFResult<ModelInfo>> + '_> {
let url = Url::parse(&format!("{}/api/models", self.endpoint()))?;
let mut query: Vec<(String, String)> = Vec::new();
if let Some(ref s) = search {
query.push(("search".into(), s.clone()));
}
if let Some(ref a) = author {
query.push(("author".into(), a.clone()));
}
if let Some(ref f) = filter {
query.push(("filter".into(), f.clone()));
}
if let Some(ref s) = sort {
query.push(("sort".into(), s.clone()));
}
if let Some(max) = limit {
if max < 1000 {
query.push(("limit".into(), max.to_string()));
}
}
if let Some(ref pt) = pipeline_tag {
query.push(("pipeline_tag".into(), pt.clone()));
}
if full == Some(true) {
query.push(("full".into(), "true".into()));
}
if card_data == Some(true) {
query.push(("cardData".into(), "true".into()));
}
if fetch_config == Some(true) {
query.push(("config".into(), "true".into()));
}
Ok(self.paginate(url, query, limit))
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_datasets(
&self,
#[builder(into)]
search: Option<String>,
#[builder(into)]
author: Option<String>,
#[builder(into)]
filter: Option<String>,
#[builder(into)]
sort: Option<String>,
full: Option<bool>,
limit: Option<usize>,
) -> HFResult<impl Stream<Item = HFResult<DatasetInfo>> + '_> {
let url = Url::parse(&format!("{}/api/datasets", self.endpoint()))?;
let mut query: Vec<(String, String)> = Vec::new();
if let Some(ref s) = search {
query.push(("search".into(), s.clone()));
}
if let Some(ref a) = author {
query.push(("author".into(), a.clone()));
}
if let Some(ref f) = filter {
query.push(("filter".into(), f.clone()));
}
if let Some(ref s) = sort {
query.push(("sort".into(), s.clone()));
}
if let Some(max) = limit
&& max < 1000
{
query.push(("limit".into(), max.to_string()));
}
if full == Some(true) {
query.push(("full".into(), "true".into()));
}
Ok(self.paginate(url, query, limit))
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_spaces(
&self,
#[builder(into)]
search: Option<String>,
#[builder(into)]
author: Option<String>,
#[builder(into)]
filter: Option<String>,
#[builder(into)]
sort: Option<String>,
full: Option<bool>,
limit: Option<usize>,
) -> HFResult<impl Stream<Item = HFResult<SpaceInfo>> + '_> {
let url = Url::parse(&format!("{}/api/spaces", self.endpoint()))?;
let mut query: Vec<(String, String)> = Vec::new();
if let Some(ref s) = search {
query.push(("search".into(), s.clone()));
}
if let Some(ref a) = author {
query.push(("author".into(), a.clone()));
}
if let Some(ref f) = filter {
query.push(("filter".into(), f.clone()));
}
if let Some(ref s) = sort {
query.push(("sort".into(), s.clone()));
}
if let Some(max) = limit
&& max < 1000
{
query.push(("limit".into(), max.to_string()));
}
if full == Some(true) {
query.push(("full".into(), "true".into()));
}
Ok(self.paginate(url, query, limit))
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn create_repository(
&self,
repo_id: &str,
repo_type: impl RepoType,
private: Option<bool>,
#[builder(default)]
exist_ok: bool,
#[builder(into)]
space_sdk: Option<String>,
) -> HFResult<RepoUrl> {
let url = format!("{}/api/repos/create", self.endpoint());
let (namespace, name) = split_repo_id(repo_id);
let mut body = serde_json::json!({
"name": name,
"private": private.unwrap_or(false),
"type": repo_type.singular(),
});
if let Some(ns) = namespace {
body["organization"] = serde_json::Value::String(ns.to_string());
}
if let Some(sdk) = space_sdk {
body["sdk"] = serde_json::Value::String(sdk);
}
let headers = self.auth_headers();
let response = retry::retry(self.retry_config(), || {
self.http_client().post(&url).headers(headers.clone()).json(&body).send()
})
.await?;
if response.status().as_u16() == 409 && exist_ok {
return Ok(RepoUrl {
url: format!("{}/{}{}", self.endpoint(), repo_type.url_prefix(), repo_id),
});
}
let response = self
.check_response(response, None, crate::error::NotFoundContext::Generic)
.await?;
Ok(response.json().await?)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn delete_repository(
&self,
repo_id: &str,
repo_type: impl RepoType,
#[builder(default)]
missing_ok: bool,
) -> HFResult<()> {
let url = format!("{}/api/repos/delete", self.endpoint());
let (namespace, name) = split_repo_id(repo_id);
let mut body = serde_json::json!({
"name": name,
"type": repo_type.singular(),
});
if let Some(ns) = namespace {
body["organization"] = serde_json::Value::String(ns.to_string());
}
let headers = self.auth_headers();
let response = retry::retry(self.retry_config(), || {
self.http_client().delete(&url).headers(headers.clone()).json(&body).send()
})
.await?;
if response.status().as_u16() == 404 && missing_ok {
return Ok(());
}
self.check_response(response, Some(repo_id), crate::error::NotFoundContext::Repo)
.await?;
Ok(())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn move_repository(
&self,
from_id: &str,
to_id: &str,
repo_type: impl RepoType,
) -> HFResult<RepoUrl> {
let url = format!("{}/api/repos/move", self.endpoint());
let body = serde_json::json!({
"fromRepo": from_id,
"toRepo": to_id,
"type": repo_type.singular(),
});
let headers = self.auth_headers();
let response = retry::retry(self.retry_config(), || {
self.http_client().post(&url).headers(headers.clone()).json(&body).send()
})
.await?;
self.check_response(response, None, crate::error::NotFoundContext::Generic)
.await?;
Ok(RepoUrl {
url: format!("{}/{}{}", self.endpoint(), repo_type.url_prefix(), to_id),
})
}
}
impl<T: RepoType> HFRepository<T> {
pub fn new(client: HFClient, repo_type: T, owner: impl Into<String>, name: impl Into<String>) -> Self {
Self {
hf_client: client,
owner: owner.into(),
name: name.into(),
repo_type,
}
}
pub fn client(&self) -> &HFClient {
&self.hf_client
}
pub fn owner(&self) -> &str {
&self.owner
}
pub fn name(&self) -> &str {
&self.name
}
pub fn repo_path(&self) -> String {
if self.owner.is_empty() {
self.name.clone()
} else {
format!("{}/{}", self.owner, self.name)
}
}
pub fn repo_type(&self) -> &T {
&self.repo_type
}
pub(crate) async fn fetch_repo_info<I: DeserializeOwned>(
&self,
revision: Option<String>,
expand: Option<Vec<String>>,
) -> HFResult<I> {
let mut url = self.hf_client.api_url(self.repo_type.plural(), &self.repo_path());
if let Some(ref revision) = revision {
url = format!("{url}/revision/{}", crate::client::encode_ref(revision));
}
let headers = self.hf_client.auth_headers();
let expand_params: Option<Vec<(&str, &str)>> =
expand.as_ref().map(|e| e.iter().map(|v| ("expand", v.as_str())).collect());
let response = retry::retry(self.hf_client.retry_config(), || {
let mut req = self.hf_client.http_client().get(&url).headers(headers.clone());
if let Some(ref params) = expand_params {
req = req.query(params);
}
req.send()
})
.await?;
let repo_path = self.repo_path();
let not_found_ctx = match revision {
Some(rev) => crate::error::NotFoundContext::Revision { revision: rev },
None => crate::error::NotFoundContext::Repo,
};
let response = self.hf_client.check_response(response, Some(&repo_path), not_found_ctx).await?;
Ok(response.json().await?)
}
}
#[bon]
impl<T: RepoType> HFRepository<T> {
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn exists(&self) -> HFResult<bool> {
let url = self.hf_client.api_url(self.repo_type.plural(), &self.repo_path());
let headers = self.hf_client.auth_headers();
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client.http_client().get(&url).headers(headers.clone()).send()
})
.await?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(false);
}
self.hf_client
.check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
.await?;
Ok(true)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn revision_exists(
&self,
revision: &str,
) -> HFResult<bool> {
let url = format!(
"{}/revision/{}",
self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
crate::client::encode_ref(revision)
);
let headers = self.hf_client.auth_headers();
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client.http_client().get(&url).headers(headers.clone()).send()
})
.await?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
return Ok(false);
}
self.hf_client
.check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
.await?;
Ok(true)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn file_exists(
&self,
filename: &str,
revision: Option<&str>,
) -> HFResult<bool> {
let revision = revision.unwrap_or(constants::DEFAULT_REVISION);
let url = self
.hf_client
.download_url(self.repo_type.url_prefix(), &self.repo_path(), revision, filename)?;
let headers = self.hf_client.auth_headers();
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client.http_client().head(&url).headers(headers.clone()).send()
})
.await?;
if response.status() == reqwest::StatusCode::NOT_FOUND {
if self.revision_exists().revision(revision).send().await? {
return Ok(false);
}
return Err(HFError::RevisionNotFound {
repo_id: self.repo_path(),
revision: revision.to_string(),
context: None,
});
}
self.hf_client
.check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
.await?;
Ok(true)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub async fn update_settings(
&self,
private: Option<bool>,
gated: Option<GatedApprovalMode>,
#[builder(into)]
description: Option<String>,
discussions_disabled: Option<bool>,
gated_notifications: Option<GatedNotifications>,
) -> HFResult<()> {
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct UpdateSettingsBody<'a> {
#[serde(skip_serializing_if = "Option::is_none")]
private: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
gated: Option<&'a GatedApprovalMode>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
discussions_disabled: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
gated_notifications_email: Option<&'a str>,
#[serde(skip_serializing_if = "Option::is_none")]
gated_notifications_mode: Option<&'a GatedNotificationsMode>,
}
let body = UpdateSettingsBody {
private,
gated: gated.as_ref(),
description: description.as_deref(),
discussions_disabled,
gated_notifications_email: gated_notifications.as_ref().and_then(|g| g.email.as_deref()),
gated_notifications_mode: gated_notifications.as_ref().map(|g| &g.mode),
};
let url = format!("{}/settings", self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()));
let headers = self.hf_client.auth_headers();
let response = retry::retry(self.hf_client.retry_config(), || {
self.hf_client
.http_client()
.put(&url)
.headers(headers.clone())
.json(&body)
.send()
})
.await?;
let repo_path = self.repo_path();
self.hf_client
.check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
.await?;
Ok(())
}
}
macro_rules! info_method_doc {
() => {
"Fetch repository metadata for this repo kind, returning the concrete info struct.\n\n\
# Parameters\n\n\
- `revision`: Git revision (branch, tag, or commit SHA). Defaults to the main branch.\n\
- `expand`: list of properties to expand in the response (e.g., `\"trendingScore\"`, `\"cardData\"`).\n \
When set, only the listed properties (plus `_id` and `id`) are returned. Kernel info ignores this."
};
}
#[bon]
impl HFRepository<RepoTypeModel> {
#[doc = info_method_doc!()]
#[builder(
finish_fn = send,
builder_type = HFModelInfoBuilder,
state_mod(name = hf_model_info_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub async fn info(
&self,
#[builder(into)]
revision: Option<String>,
expand: Option<Vec<String>>,
) -> HFResult<ModelInfo> {
self.fetch_repo_info(revision, expand).await
}
}
#[bon]
impl HFRepository<RepoTypeDataset> {
#[doc = info_method_doc!()]
#[builder(
finish_fn = send,
builder_type = HFDatasetInfoBuilder,
state_mod(name = hf_dataset_info_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub async fn info(
&self,
#[builder(into)]
revision: Option<String>,
expand: Option<Vec<String>>,
) -> HFResult<DatasetInfo> {
self.fetch_repo_info(revision, expand).await
}
}
#[bon]
impl HFRepository<RepoTypeSpace> {
#[doc = info_method_doc!()]
#[builder(
finish_fn = send,
builder_type = HFSpaceInfoBuilder,
state_mod(name = hf_space_info_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub async fn info(
&self,
#[builder(into)]
revision: Option<String>,
expand: Option<Vec<String>>,
) -> HFResult<SpaceInfo> {
self.fetch_repo_info(revision, expand).await
}
}
#[bon]
impl HFRepository<RepoTypeKernel> {
#[doc = info_method_doc!()]
#[builder(
finish_fn = send,
builder_type = HFKernelInfoBuilder,
state_mod(name = hf_kernel_info_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub async fn info(
&self,
#[builder(into)]
revision: Option<String>,
expand: Option<Vec<String>>,
) -> HFResult<KernelInfo> {
self.fetch_repo_info(revision, expand).await
}
}
fn split_repo_id(repo_id: &str) -> (Option<&str>, &str) {
match repo_id.split_once('/') {
Some((ns, name)) => (Some(ns), name),
None => (None, repo_id),
}
}
#[cfg(feature = "blocking")]
#[bon]
impl crate::blocking::HFClientSync {
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_models(
&self,
#[builder(into)] search: Option<String>,
#[builder(into)] author: Option<String>,
#[builder(into)] filter: Option<String>,
#[builder(into)] sort: Option<String>,
#[builder(into)] pipeline_tag: Option<String>,
full: Option<bool>,
card_data: Option<bool>,
fetch_config: Option<bool>,
limit: Option<usize>,
) -> HFResult<Vec<ModelInfo>> {
use futures::StreamExt;
self.runtime.block_on(async move {
let stream = self
.inner
.list_models()
.maybe_search(search)
.maybe_author(author)
.maybe_filter(filter)
.maybe_sort(sort)
.maybe_pipeline_tag(pipeline_tag)
.maybe_full(full)
.maybe_card_data(card_data)
.maybe_fetch_config(fetch_config)
.maybe_limit(limit)
.send()?;
futures::pin_mut!(stream);
let mut items = Vec::new();
while let Some(item) = stream.next().await {
items.push(item?);
}
Ok(items)
})
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_datasets(
&self,
#[builder(into)] search: Option<String>,
#[builder(into)] author: Option<String>,
#[builder(into)] filter: Option<String>,
#[builder(into)] sort: Option<String>,
full: Option<bool>,
limit: Option<usize>,
) -> HFResult<Vec<DatasetInfo>> {
use futures::StreamExt;
self.runtime.block_on(async move {
let stream = self
.inner
.list_datasets()
.maybe_search(search)
.maybe_author(author)
.maybe_filter(filter)
.maybe_sort(sort)
.maybe_full(full)
.maybe_limit(limit)
.send()?;
futures::pin_mut!(stream);
let mut items = Vec::new();
while let Some(item) = stream.next().await {
items.push(item?);
}
Ok(items)
})
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn list_spaces(
&self,
#[builder(into)] search: Option<String>,
#[builder(into)] author: Option<String>,
#[builder(into)] filter: Option<String>,
#[builder(into)] sort: Option<String>,
full: Option<bool>,
limit: Option<usize>,
) -> HFResult<Vec<SpaceInfo>> {
use futures::StreamExt;
self.runtime.block_on(async move {
let stream = self
.inner
.list_spaces()
.maybe_search(search)
.maybe_author(author)
.maybe_filter(filter)
.maybe_sort(sort)
.maybe_full(full)
.maybe_limit(limit)
.send()?;
futures::pin_mut!(stream);
let mut items = Vec::new();
while let Some(item) = stream.next().await {
items.push(item?);
}
Ok(items)
})
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn create_repository(
&self,
repo_id: &str,
repo_type: impl RepoType,
private: Option<bool>,
#[builder(default)] exist_ok: bool,
#[builder(into)] space_sdk: Option<String>,
) -> HFResult<RepoUrl> {
self.runtime.block_on(
self.inner
.create_repository()
.repo_id(repo_id)
.repo_type(repo_type)
.maybe_private(private)
.exist_ok(exist_ok)
.maybe_space_sdk(space_sdk)
.send(),
)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn delete_repository(
&self,
repo_id: &str,
repo_type: impl RepoType,
#[builder(default)] missing_ok: bool,
) -> HFResult<()> {
self.runtime.block_on(
self.inner
.delete_repository()
.repo_id(repo_id)
.repo_type(repo_type)
.missing_ok(missing_ok)
.send(),
)
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn move_repository(&self, from_id: &str, to_id: &str, repo_type: impl RepoType) -> HFResult<RepoUrl> {
self.runtime.block_on(
self.inner
.move_repository()
.from_id(from_id)
.to_id(to_id)
.repo_type(repo_type)
.send(),
)
}
}
#[cfg(feature = "blocking")]
#[bon]
impl<T: RepoType> crate::blocking::HFRepositorySync<T> {
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn exists(&self) -> HFResult<bool> {
self.runtime.block_on(self.inner.exists().send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn revision_exists(&self, revision: &str) -> HFResult<bool> {
self.runtime.block_on(self.inner.revision_exists().revision(revision).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn file_exists(&self, filename: &str, revision: Option<&str>) -> HFResult<bool> {
self.runtime
.block_on(self.inner.file_exists().filename(filename).maybe_revision(revision).send())
}
#[builder(finish_fn = send, derive(Debug, Clone))]
pub fn update_settings(
&self,
private: Option<bool>,
gated: Option<GatedApprovalMode>,
#[builder(into)] description: Option<String>,
discussions_disabled: Option<bool>,
gated_notifications: Option<GatedNotifications>,
) -> HFResult<()> {
self.runtime.block_on(
self.inner
.update_settings()
.maybe_private(private)
.maybe_gated(gated)
.maybe_description(description)
.maybe_discussions_disabled(discussions_disabled)
.maybe_gated_notifications(gated_notifications)
.send(),
)
}
}
#[cfg(feature = "blocking")]
#[bon]
impl crate::blocking::HFRepositorySync<RepoTypeModel> {
#[builder(
finish_fn = send,
builder_type = HFModelInfoSyncBuilder,
state_mod(name = hf_model_info_sync_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<ModelInfo> {
self.runtime
.block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
}
}
#[cfg(feature = "blocking")]
#[bon]
impl crate::blocking::HFRepositorySync<RepoTypeDataset> {
#[builder(
finish_fn = send,
builder_type = HFDatasetInfoSyncBuilder,
state_mod(name = hf_dataset_info_sync_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub fn info(
&self,
#[builder(into)] revision: Option<String>,
expand: Option<Vec<String>>,
) -> HFResult<DatasetInfo> {
self.runtime
.block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
}
}
#[cfg(feature = "blocking")]
#[bon]
impl crate::blocking::HFRepositorySync<RepoTypeSpace> {
#[builder(
finish_fn = send,
builder_type = HFSpaceInfoSyncBuilder,
state_mod(name = hf_space_info_sync_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<SpaceInfo> {
self.runtime
.block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
}
}
#[cfg(feature = "blocking")]
#[bon]
impl crate::blocking::HFRepositorySync<RepoTypeKernel> {
#[builder(
finish_fn = send,
builder_type = HFKernelInfoSyncBuilder,
state_mod(name = hf_kernel_info_sync_builder, vis = "pub(crate)"),
derive(Debug, Clone),
)]
pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<KernelInfo> {
self.runtime
.block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
}
}
#[cfg(test)]
mod tests {
use futures::StreamExt;
use super::{
DatasetInfo, EvalResultEntry, HFRepository, InferenceProviderMapping, KernelInfo, ModelInfo, RepoType,
RepoTypeDataset, RepoTypeKernel, RepoTypeModel, RepoTypeSpace, SafeTensorsInfo, SpaceInfo, TransformersInfo,
split_repo_id,
};
use crate::client::HFClient;
#[test]
fn test_repo_path_and_accessors() {
let client = HFClient::builder().build().unwrap();
let repo: HFRepository<RepoTypeModel> = HFRepository::new(client, RepoTypeModel, "openai-community", "gpt2");
assert_eq!(repo.owner(), "openai-community");
assert_eq!(repo.name(), "gpt2");
assert_eq!(repo.repo_path(), "openai-community/gpt2");
assert_eq!(repo.repo_type().singular(), "model");
}
#[test]
fn test_marker_struct_singular_and_plural() {
assert_eq!(RepoTypeModel.singular(), "model");
assert_eq!(RepoTypeDataset.singular(), "dataset");
assert_eq!(RepoTypeSpace.singular(), "space");
assert_eq!(RepoTypeKernel.singular(), "kernel");
assert_eq!(RepoTypeModel.plural(), "models");
assert_eq!(RepoTypeDataset.plural(), "datasets");
assert_eq!(RepoTypeSpace.plural(), "spaces");
assert_eq!(RepoTypeKernel.plural(), "kernels");
}
#[test]
fn test_marker_struct_url_prefix() {
assert_eq!(RepoTypeModel.url_prefix(), "");
assert_eq!(RepoTypeDataset.url_prefix(), "datasets/");
assert_eq!(RepoTypeSpace.url_prefix(), "spaces/");
assert_eq!(RepoTypeKernel.url_prefix(), "kernels/");
}
#[test]
fn test_marker_struct_display() {
assert_eq!(format!("{}", RepoTypeModel), "model");
assert_eq!(format!("{}", RepoTypeDataset), "dataset");
assert_eq!(format!("{}", RepoTypeSpace), "space");
assert_eq!(format!("{}", RepoTypeKernel), "kernel");
}
#[test]
fn test_split_repo_id() {
assert_eq!(split_repo_id("user/repo"), (Some("user"), "repo"));
assert_eq!(split_repo_id("repo"), (None, "repo"));
assert_eq!(split_repo_id("org/sub/repo"), (Some("org"), "sub/repo"));
}
#[tokio::test]
async fn test_list_models_limit_zero_returns_empty() {
let client = HFClient::builder().build().unwrap();
let stream = client.list_models().limit(0_usize).send().unwrap();
futures::pin_mut!(stream);
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn test_list_datasets_limit_zero_returns_empty() {
let client = HFClient::builder().build().unwrap();
let stream = client.list_datasets().limit(0_usize).send().unwrap();
futures::pin_mut!(stream);
assert!(stream.next().await.is_none());
}
#[tokio::test]
async fn test_list_spaces_limit_zero_returns_empty() {
let client = HFClient::builder().build().unwrap();
let stream = client.list_spaces().limit(0_usize).send().unwrap();
futures::pin_mut!(stream);
assert!(stream.next().await.is_none());
}
#[test]
fn test_safetensors_info_deserialize() {
let json = r#"{"parameters":{"F32":124000000,"BF16":1000000},"total":125000000}"#;
let info: SafeTensorsInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.total, 125_000_000);
assert_eq!(info.parameters.get("F32"), Some(&124_000_000));
}
#[test]
fn test_transformers_info_deserialize() {
let json = r#"{"auto_model":"AutoModelForCausalLM","pipeline_tag":"text-generation"}"#;
let info: TransformersInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.auto_model, "AutoModelForCausalLM");
assert_eq!(info.pipeline_tag.as_deref(), Some("text-generation"));
assert!(info.processor.is_none());
}
#[test]
fn test_eval_result_entry_minimal() {
let json = r#"{"dataset":{"id":"cais/hle","task_id":"default"},"value":20.9}"#;
let entry: EvalResultEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.dataset.id, "cais/hle");
assert_eq!(entry.dataset.task_id, "default");
assert_eq!(entry.value.as_f64(), Some(20.9));
assert!(entry.source.is_none());
}
#[test]
fn test_eval_result_entry_with_source() {
let json = r#"{"dataset":{"id":"d/x","task_id":"t","revision":"abc"},"value":0.5,"source":{"url":"u","name":"n","org":"o"},"verifyToken":"vt","notes":"n"}"#;
let entry: EvalResultEntry = serde_json::from_str(json).unwrap();
assert_eq!(entry.dataset.id, "d/x");
assert_eq!(entry.dataset.revision.as_deref(), Some("abc"));
let source = entry.source.as_ref().unwrap();
assert_eq!(source.url.as_deref(), Some("u"));
assert_eq!(source.org.as_deref(), Some("o"));
assert_eq!(entry.verify_token.as_deref(), Some("vt"));
}
#[test]
fn test_inference_provider_mapping_list_form() {
let json = r#"{
"id":"o/m",
"inferenceProviderMapping":[
{"provider":"hf-inference","providerId":"o/m","status":"live","task":"text-generation"}
]
}"#;
let info: ModelInfo = serde_json::from_str(json).unwrap();
let mappings = info.inference_provider_mapping.unwrap();
assert_eq!(mappings.len(), 1);
assert_eq!(mappings[0].provider, "hf-inference");
assert_eq!(mappings[0].provider_id, "o/m");
assert_eq!(mappings[0].status, "live");
}
#[test]
fn test_inference_provider_mapping_dict_form() {
let json = r#"{
"id":"o/m",
"inferenceProviderMapping":{
"together":{"providerId":"o/m","status":"live","task":"text-generation"}
}
}"#;
let info: ModelInfo = serde_json::from_str(json).unwrap();
let mappings = info.inference_provider_mapping.unwrap();
assert_eq!(mappings.len(), 1);
assert_eq!(mappings[0].provider, "together");
assert_eq!(mappings[0].task, "text-generation");
}
#[test]
fn test_inference_provider_mapping_helper_directly() {
let info_helper: InferenceProviderMapping = serde_json::from_str(
r#"{"provider":"x","providerId":"y","status":"live","task":"t","adapterWeightsPath":"w"}"#,
)
.unwrap();
assert_eq!(info_helper.adapter_weights_path.as_deref(), Some("w"));
}
#[test]
fn test_model_info_ignores_unknown_and_legacy_fields() {
let json = r#"{"id":"o/m","modelId":"o/m","brandNewField":42}"#;
let info: ModelInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.id, "o/m");
}
#[test]
fn test_kernel_info_deserializes_real_response() {
let json = r#"{
"_id":"69d02879cbdc347de53cced2",
"author":"kernels-community",
"authorData":{"name":"kernels-community","type":"org"},
"trustedPublisher":false,
"downloads":7199,
"gated":false,
"id":"kernels-community/flash-attn2",
"isLikedByUser":false,
"lastModified":"2026-04-20T20:31:57.000Z",
"likes":6,
"private":false,
"repoType":"kernel",
"sha":"e16b327d7c5b015cac48944d4058f688e4d0c62f",
"supportedDriverFamilies":["cuda","xpu","cpu"]
}"#;
let info: KernelInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.id, "kernels-community/flash-attn2");
assert_eq!(info.author.as_deref(), Some("kernels-community"));
assert_eq!(info.sha.as_deref(), Some("e16b327d7c5b015cac48944d4058f688e4d0c62f"));
assert_eq!(info.downloads, Some(7199));
assert_eq!(info.likes, Some(6));
assert_eq!(info.trusted_publisher, Some(false));
assert_eq!(info.supported_driver_families.as_deref(), Some(&["cuda".into(), "xpu".into(), "cpu".into()][..]));
assert_eq!(info.gated.as_ref().and_then(|v| v.as_bool()), Some(false));
}
#[test]
fn test_kernel_info_missing_supported_driver_families() {
let json = r#"{"id":"o/k","sha":"abc","downloads":0,"likes":0,"private":false,"gated":false}"#;
let info: KernelInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.id, "o/k");
assert!(info.supported_driver_families.is_none());
}
#[test]
fn test_dataset_info_new_fields() {
let json = r#"{
"id":"u/d",
"citation":"Doe et al. 2024",
"paperswithcode_id":"pwc-id",
"resourceGroup":{"id":"rg-1","name":"Team A"}
}"#;
let info: DatasetInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.citation.as_deref(), Some("Doe et al. 2024"));
assert_eq!(info.paperswithcode_id.as_deref(), Some("pwc-id"));
assert!(info.resource_group.is_some());
}
#[test]
fn test_space_info_new_fields() {
let json = r#"{
"id":"u/s",
"models":["org/model-a","org/model-b"],
"datasets":["org/dataset"],
"resourceGroup":{"id":"rg-2"}
}"#;
let info: SpaceInfo = serde_json::from_str(json).unwrap();
assert_eq!(info.models.as_deref(), Some(&["org/model-a".to_string(), "org/model-b".to_string()][..]));
assert_eq!(info.datasets.as_deref(), Some(&["org/dataset".to_string()][..]));
assert!(info.resource_group.is_some());
}
}