Skip to main content

hf_hub/repository/
mod.rs

1//! Repository handles, metadata types, and list/create/delete/move APIs.
2//!
3//! Start from [`HFClient`] — [`HFClient::model`], [`HFClient::dataset`],
4//! [`HFClient::space`], and [`HFClient::kernel`] return a typed
5//! [`HFRepository<T>`](HFRepository) for the corresponding repo kind. All read/write
6//! file and revision APIs hang off that value; methods that differ per repo kind (such
7//! as [`info`](HFRepository::info)) are resolved at compile time via the [`RepoType`]
8//! type parameter.
9//!
10//! Submodule pages group related builders and types:
11//!
12//! - [`commits`] — history, refs, compare/diff, branches, tags
13//! - [`listing`] — list files/tree, path metadata
14//! - [`download`] — single-file and snapshot download builders
15//! - [`upload`] — uploads, deletes, and [`CommitOperation`] batches
16//! - [`diff`] — parsed raw diff lines ([`HFFileDiff`])
17//! - [`files`] — shared types such as [`CommitOperation`] and [`RepoTreeEntry`]
18//!
19//! Most items are also re-exported at [`crate::repository`] for a flat `hf_hub::repository::…`
20//! path in addition to the submodule paths rustdoc lists.
21
22pub mod commits;
23pub mod diff;
24pub mod download;
25pub mod files;
26pub mod listing;
27pub mod repo_type;
28pub mod upload;
29
30use std::collections::HashMap;
31use std::str::FromStr;
32
33use bon::bon;
34pub use commits::{CommitAuthor, DiffEntry, GitCommitInfo, GitRefInfo, GitRefs};
35pub use diff::{GitStatus, HFDiffParseError, HFFileDiff};
36pub use files::{
37    AddSource, BlobLfsInfo, BlobSecurityInfo, CommitInfo, CommitOperation, FileMetadataInfo, LastCommitInfo,
38    RepoTreeEntry, SourceByteStream, StreamFactory, StreamSource,
39};
40#[cfg(not(target_family = "wasm"))]
41pub(crate) use files::{extract_file_size, extract_xet_hash};
42use futures::Stream;
43pub use repo_type::{RepoType, RepoTypeAny, RepoTypeDataset, RepoTypeKernel, RepoTypeModel, RepoTypeSpace};
44use serde::de::DeserializeOwned;
45use serde::{Deserialize, Serialize, Serializer};
46use url::Url;
47
48use crate::client::HFClient;
49use crate::error::{HFError, HFResult};
50use crate::{constants, retry};
51
52/// Access-gating mode for a repository.
53///
54/// Controls whether users must request access and how requests are approved.
55/// Serializes as `false` when [`GatedApprovalMode::Disabled`], or as the lowercase mode string otherwise.
56#[derive(Debug, Clone)]
57pub enum GatedApprovalMode {
58    /// Access is open; no request is required.
59    Disabled,
60    /// Access requests are approved automatically once the user accepts the terms.
61    Auto,
62    /// Access requests must be reviewed and approved by a repo owner.
63    Manual,
64}
65
66/// Notification cadence for gated-access requests on a repository.
67#[derive(Debug, Clone, Serialize)]
68#[serde(rename_all = "kebab-case")]
69pub enum GatedNotificationsMode {
70    /// Bundle notifications and deliver them periodically.
71    Bulk,
72    /// Notify on every access request as it arrives.
73    RealTime,
74}
75
76/// Notification preferences for gated-access requests on a repository.
77///
78/// Groups the cadence (`mode`) with the optional override `email`. Pass to
79/// [`HFRepository::update_settings`] via the `gated_notifications` parameter so
80/// the two fields move together — leaving the email out keeps the existing
81/// recipient.
82#[derive(Debug, Clone)]
83pub struct GatedNotifications {
84    /// Cadence at which gated-access notifications are sent.
85    pub mode: GatedNotificationsMode,
86    /// Override the email address that receives gated-access notifications.
87    /// When `None`, the existing recipient configured on the repository is left in place.
88    pub email: Option<String>,
89}
90
91impl GatedNotifications {
92    /// Construct a notification configuration with just the cadence.
93    pub fn new(mode: GatedNotificationsMode) -> Self {
94        Self { mode, email: None }
95    }
96
97    /// Set or replace the override email recipient for gated-access notifications.
98    pub fn with_email(mut self, email: impl Into<String>) -> Self {
99        self.email = Some(email.into());
100        self
101    }
102}
103
104/// A single file entry in a repository's flat "siblings" listing, as returned by the repo info endpoint.
105///
106/// Most fields are populated only when the repo info request asks for file metadata (the `files_metadata`
107/// option in the Python client / `?blobs=true` on the API). When listing repos, only `rfilename` is set.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct RepoSibling {
110    /// File path relative to the repo root.
111    pub rfilename: String,
112    /// File size in bytes. Populated only when file metadata was requested.
113    pub size: Option<u64>,
114    /// LFS metadata for the file. Populated only when the file is stored with Git LFS and file metadata was
115    /// requested.
116    pub lfs: Option<BlobLfsInfo>,
117}
118
119/// SafeTensors footprint for a model: per-dtype parameter counts and total.
120#[derive(Debug, Clone, Deserialize, Serialize)]
121pub struct SafeTensorsInfo {
122    /// Parameter counts keyed by dtype (e.g., `"F32"`, `"BF16"`, `"I8"`).
123    pub parameters: HashMap<String, u64>,
124    /// Total number of parameters across all dtypes.
125    pub total: u64,
126}
127
128/// Transformers-specific metadata for a model (auto class, processor, etc.).
129#[derive(Debug, Clone, Deserialize, Serialize)]
130#[serde(rename_all = "snake_case")]
131pub struct TransformersInfo {
132    /// Name of the Transformers auto class for this model (e.g., `"AutoModelForCausalLM"`).
133    pub auto_model: String,
134    /// Custom Python class declared by the model, if any.
135    #[serde(default)]
136    pub custom_class: Option<String>,
137    /// Pipeline tag declared in `transformersInfo` (may differ from the top-level `pipeline_tag`).
138    #[serde(default)]
139    pub pipeline_tag: Option<String>,
140    /// Processor name declared by the model, if any.
141    #[serde(default)]
142    pub processor: Option<String>,
143}
144
145/// Inference-providers mapping for a model.
146///
147/// Mirrors `huggingface_hub.InferenceProviderMapping`. Each entry describes how a single provider
148/// serves the model.
149#[derive(Debug, Clone, Deserialize, Serialize)]
150#[serde(rename_all = "camelCase")]
151pub struct InferenceProviderMapping {
152    /// Provider name (e.g., `"hf-inference"`, `"together"`).
153    pub provider: String,
154    /// ID of the model on the provider's side.
155    pub provider_id: String,
156    /// Status of the mapping: `"error"`, `"live"`, or `"staging"`.
157    pub status: String,
158    /// Task served by this provider (e.g., `"text-generation"`).
159    pub task: String,
160    /// Adapter name, if the mapping uses an adapter.
161    #[serde(default)]
162    pub adapter: Option<String>,
163    /// Path to adapter weights, if applicable.
164    #[serde(default)]
165    pub adapter_weights_path: Option<String>,
166    /// Mapping kind: `"single-model"` or `"tag-filter"`.
167    #[serde(default, rename = "type")]
168    pub r#type: Option<String>,
169}
170
171// The Hub returns `inferenceProviderMapping` as an object keyed by provider name on
172// `GET /api/models/{id}` and as an array on `GET /api/models`. See:
173//   https://huggingface.co/docs/inference-providers/hub-api
174//   https://huggingface.co/docs/inference-providers/register-as-a-provider
175// The Python `huggingface_hub` library handles both shapes the same way and notes:
176// "a dict on model_info and a list on list_models. Let's harmonize to list."
177fn deserialize_inference_provider_mapping<'de, D>(
178    deserializer: D,
179) -> Result<Option<Vec<InferenceProviderMapping>>, D::Error>
180where
181    D: serde::de::Deserializer<'de>,
182{
183    use serde::de::Error;
184    let value = Option::<serde_json::Value>::deserialize(deserializer)?;
185    let Some(value) = value else { return Ok(None) };
186    match value {
187        serde_json::Value::Null => Ok(None),
188        serde_json::Value::Array(items) => {
189            let mut out = Vec::with_capacity(items.len());
190            for item in items {
191                out.push(serde_json::from_value(item).map_err(D::Error::custom)?);
192            }
193            Ok(Some(out))
194        },
195        serde_json::Value::Object(map) => {
196            let mut out = Vec::with_capacity(map.len());
197            for (provider, mut value) in map {
198                if let serde_json::Value::Object(ref mut obj) = value {
199                    obj.insert("provider".to_string(), serde_json::Value::String(provider));
200                }
201                out.push(serde_json::from_value(value).map_err(D::Error::custom)?);
202            }
203            Ok(Some(out))
204        },
205        _ => Err(D::Error::custom("expected list or object for inferenceProviderMapping")),
206    }
207}
208
209/// One evaluation-result entry from the `.eval_results/*.yaml` format.
210///
211/// See <https://huggingface.co/docs/hub/eval-results>.
212#[derive(Debug, Clone, Deserialize, Serialize)]
213pub struct EvalResultEntry {
214    /// Benchmark dataset and task identifiers.
215    pub dataset: EvalResultDataset,
216    /// The metric value (numeric, string, or other JSON shape depending on the benchmark).
217    pub value: serde_json::Value,
218    /// Signature proving the evaluation is auditable and reproducible.
219    #[serde(default, rename = "verifyToken")]
220    pub verify_token: Option<String>,
221    /// ISO-8601 datetime when the evaluation was run; defaults to git commit time when omitted.
222    #[serde(default)]
223    pub date: Option<String>,
224    /// Source attribution for the evaluation (Space, dataset, user, org), if any.
225    #[serde(default)]
226    pub source: Option<EvalResultSource>,
227    /// Free-text notes about the evaluation setup.
228    #[serde(default)]
229    pub notes: Option<String>,
230}
231
232/// Benchmark dataset and task identifiers for an [`EvalResultEntry`].
233#[derive(Debug, Clone, Deserialize, Serialize)]
234pub struct EvalResultDataset {
235    /// Benchmark dataset ID (e.g., `"cais/hle"`).
236    pub id: String,
237    /// Task identifier within the benchmark (e.g., `"gpqa_diamond"`).
238    pub task_id: String,
239    /// Git SHA of the benchmark dataset, if pinned.
240    #[serde(default)]
241    pub revision: Option<String>,
242}
243
244/// Source attribution for an [`EvalResultEntry`].
245#[derive(Debug, Clone, Deserialize, Serialize)]
246pub struct EvalResultSource {
247    /// URL pointing to the evaluation source (Space, dataset, etc.).
248    #[serde(default)]
249    pub url: Option<String>,
250    /// Display name for the source.
251    #[serde(default)]
252    pub name: Option<String>,
253    /// HF user attributed for the evaluation.
254    #[serde(default)]
255    pub user: Option<String>,
256    /// HF org attributed for the evaluation.
257    #[serde(default)]
258    pub org: Option<String>,
259}
260
261/// Metadata for a model repository on the Hub.
262///
263/// Returned by [`HFClient::list_models`] and by
264/// [`HFRepository::info`] when the repo is a model.
265/// Most fields are optional because they depend on the `expand` parameter and the repo's state.
266#[derive(Debug, Clone, Deserialize, Serialize)]
267#[serde(rename_all = "camelCase")]
268pub struct ModelInfo {
269    /// Repo ID, in the form `owner/name`.
270    pub id: String,
271    /// Internal Hub identifier (the API's `_id` field). Most callers should use `id` instead.
272    #[serde(rename = "_id")]
273    pub internal_id: Option<String>,
274    /// Owner of the repo (the part before `/` in `id`).
275    pub author: Option<String>,
276    /// Base models this model is derived from.
277    #[serde(default)]
278    pub base_models: Option<Vec<String>>,
279    /// Parsed YAML metadata from the model card (`README.md` front matter). Modeled as raw JSON
280    /// because the schema varies by library.
281    pub card_data: Option<serde_json::Value>,
282    /// Number of children (derived) models.
283    pub children_model_count: Option<u64>,
284    /// Model configuration (e.g., parsed `config.json` for Transformers models).
285    pub config: Option<serde_json::Value>,
286    /// ISO-8601 timestamp when the repo was created. The earliest possible value is
287    /// `2022-03-02T23:29:04.000Z` (when the Hub started recording creation dates).
288    pub created_at: Option<String>,
289    /// Whether the repo is disabled.
290    pub disabled: Option<bool>,
291    /// Number of downloads over the last 30 days.
292    pub downloads: Option<u64>,
293    /// Cumulative download count since repo creation.
294    pub downloads_all_time: Option<u64>,
295    /// Evaluation results parsed from the model's `.eval_results/*.yaml` files.
296    #[serde(default, rename = "evalResults")]
297    pub eval_results: Option<Vec<EvalResultEntry>>,
298    /// Gated-access state. Either the boolean `false` (open) or the string `"auto"`/`"manual"` indicating
299    /// the approval mode for access requests. Modeled as raw JSON because the field is union-typed.
300    pub gated: Option<serde_json::Value>,
301    /// GGUF-specific metadata, when the repo contains GGUF files.
302    pub gguf: Option<serde_json::Value>,
303    /// Inference-providers status. Currently, `Some("warm")` when the model is served by at least
304    /// one provider, `None` otherwise.
305    pub inference: Option<String>,
306    /// Per-provider inference mappings, ordered by the user's provider preference.
307    ///
308    /// The Hub returns this either as a list (modern shape) or as an object keyed by provider name
309    /// (legacy shape); both are accepted.
310    #[serde(default, deserialize_with = "deserialize_inference_provider_mapping")]
311    pub inference_provider_mapping: Option<Vec<InferenceProviderMapping>>,
312    /// ISO-8601 timestamp of the most recent commit to the repo.
313    pub last_modified: Option<String>,
314    /// Library this model is associated with (e.g., `"transformers"`, `"diffusers"`).
315    #[serde(rename = "library_name")]
316    pub library_name: Option<String>,
317    /// Number of likes on the repo.
318    pub likes: Option<u64>,
319    /// Mask token used by the model (for fill-mask tasks).
320    #[serde(rename = "mask_token")]
321    pub mask_token: Option<String>,
322    /// Model-index data describing benchmark results in the `model-index` format.
323    #[serde(rename = "model-index")]
324    pub model_index: Option<serde_json::Value>,
325    /// Primary task tag (e.g., `"text-generation"`, `"image-classification"`).
326    #[serde(rename = "pipeline_tag")]
327    pub pipeline_tag: Option<String>,
328    /// Whether the repo is private.
329    pub private: Option<bool>,
330    /// Resource-group information, when the repo belongs to one.
331    pub resource_group: Option<serde_json::Value>,
332    /// Per-dtype parameter counts produced from the model's safetensors files, if any.
333    pub safetensors: Option<SafeTensorsInfo>,
334    /// Security-scan summary for the repo.
335    pub security_repo_status: Option<serde_json::Value>,
336    /// Git commit SHA at the revision the response describes.
337    pub sha: Option<String>,
338    /// Files in the repo. Only populated when file metadata is requested; otherwise only `rfilename`
339    /// is set on each entry. See [`RepoSibling`].
340    pub siblings: Option<Vec<RepoSibling>>,
341    /// IDs of Spaces that use this model.
342    pub spaces: Option<Vec<String>>,
343    /// Hub tags. Includes both author-provided tags from the model card and tags computed by the Hub
344    /// (e.g., supported libraries, arXiv references).
345    pub tags: Option<Vec<String>>,
346    /// Transformers-specific metadata declared by the model.
347    pub transformers_info: Option<TransformersInfo>,
348    /// Trending score used to rank the repo on the Hub's trending lists.
349    pub trending_score: Option<f64>,
350    /// Total size of the repo on disk, in bytes.
351    pub used_storage: Option<u64>,
352    /// Inference-widget configuration declared in the model card.
353    pub widget_data: Option<serde_json::Value>,
354}
355
356/// Metadata for a dataset repository on the Hub.
357///
358/// Returned by [`HFClient::list_datasets`] and by
359/// [`HFRepository::info`] when the repo is a dataset.
360/// Most fields are optional because they depend on the `expand` parameter and the repo's state.
361#[derive(Debug, Clone, Deserialize, Serialize)]
362#[serde(rename_all = "camelCase")]
363pub struct DatasetInfo {
364    /// Repo ID, in the form `owner/name`.
365    pub id: String,
366    /// Internal Hub identifier (the API's `_id` field). Most callers should use `id` instead.
367    #[serde(rename = "_id")]
368    pub internal_id: Option<String>,
369    /// Owner of the repo (the part before `/` in `id`).
370    pub author: Option<String>,
371    /// Git commit SHA at the revision the response describes.
372    pub sha: Option<String>,
373    /// Whether the repo is private.
374    pub private: Option<bool>,
375    /// Gated-access state. Either the boolean `false` (open) or the string `"auto"`/`"manual"` indicating
376    /// the approval mode for access requests. Modeled as raw JSON because the field is union-typed.
377    pub gated: Option<serde_json::Value>,
378    /// Whether the repo is disabled.
379    pub disabled: Option<bool>,
380    /// Number of downloads over the last 30 days.
381    pub downloads: Option<u64>,
382    /// Cumulative download count since repo creation.
383    pub downloads_all_time: Option<u64>,
384    /// Number of likes on the repo.
385    pub likes: Option<u64>,
386    /// Hub tags declared on the dataset.
387    pub tags: Option<Vec<String>>,
388    /// ISO-8601 timestamp when the repo was created. The earliest possible value is
389    /// `2022-03-02T23:29:04.000Z` (when the Hub started recording creation dates).
390    pub created_at: Option<String>,
391    /// ISO-8601 timestamp of the most recent commit to the repo.
392    pub last_modified: Option<String>,
393    /// Files in the repo. Only populated when file metadata is requested; otherwise only `rfilename`
394    /// is set on each entry. See [`RepoSibling`].
395    pub siblings: Option<Vec<RepoSibling>>,
396    /// Parsed YAML metadata from the dataset card (`README.md` front matter).
397    pub card_data: Option<serde_json::Value>,
398    /// Citation information for the dataset.
399    pub citation: Option<String>,
400    /// Papers-with-code identifier, when the dataset is registered there.
401    #[serde(rename = "paperswithcode_id")]
402    pub paperswithcode_id: Option<String>,
403    /// Resource-group information, when the repo belongs to one.
404    pub resource_group: Option<serde_json::Value>,
405    /// Trending score used to rank the repo on the Hub's trending lists.
406    pub trending_score: Option<f64>,
407    /// Free-text description of the dataset.
408    pub description: Option<String>,
409    /// Total size of the repo on disk, in bytes.
410    pub used_storage: Option<u64>,
411}
412
413/// Metadata for a Space repository on the Hub.
414///
415/// Returned by [`HFClient::list_spaces`] and by
416/// [`HFRepository::info`] when the repo is a Space.
417/// Most fields are optional because they depend on the `expand` parameter and the Space's state.
418#[derive(Debug, Clone, Deserialize, Serialize)]
419#[serde(rename_all = "camelCase")]
420pub struct SpaceInfo {
421    /// Repo ID, in the form `owner/name`.
422    pub id: String,
423    /// Internal Hub identifier (the API's `_id` field). Most callers should use `id` instead.
424    #[serde(rename = "_id")]
425    pub internal_id: Option<String>,
426    /// Owner of the repo (the part before `/` in `id`).
427    pub author: Option<String>,
428    /// Git commit SHA at the revision the response describes.
429    pub sha: Option<String>,
430    /// Whether the repo is private.
431    pub private: Option<bool>,
432    /// Gated-access state. Either the boolean `false` (open) or the string `"auto"`/`"manual"` indicating
433    /// the approval mode for access requests. Modeled as raw JSON because the field is union-typed.
434    pub gated: Option<serde_json::Value>,
435    /// Whether the Space is disabled.
436    pub disabled: Option<bool>,
437    /// Number of likes on the Space.
438    pub likes: Option<u64>,
439    /// Hub tags declared on the Space.
440    pub tags: Option<Vec<String>>,
441    /// ISO-8601 timestamp when the repo was created. The earliest possible value is
442    /// `2022-03-02T23:29:04.000Z` (when the Hub started recording creation dates).
443    pub created_at: Option<String>,
444    /// ISO-8601 timestamp of the most recent commit to the repo.
445    pub last_modified: Option<String>,
446    /// Files in the repo. Only populated when file metadata is requested; otherwise only `rfilename`
447    /// is set on each entry. See [`RepoSibling`].
448    pub siblings: Option<Vec<RepoSibling>>,
449    /// Parsed YAML metadata from the Space card (`README.md` front matter).
450    pub card_data: Option<serde_json::Value>,
451    /// SDK powering the Space (e.g., `"gradio"`, `"streamlit"`, `"docker"`, `"static"`).
452    pub sdk: Option<String>,
453    /// Trending score used to rank the Space on the Hub's trending lists.
454    pub trending_score: Option<f64>,
455    /// Hostname serving the Space.
456    pub host: Option<String>,
457    /// Subdomain serving the Space.
458    pub subdomain: Option<String>,
459    /// Runtime state of the Space (stage, hardware, sleep time, volumes, etc.).
460    ///
461    /// Populated when the repo info request expands the `runtime` field. The same shape is also
462    /// returned by [`HFRepository::<RepoTypeSpace>::runtime`](HFRepository::runtime).
463    pub runtime: Option<crate::spaces::SpaceRuntime>,
464    /// Datasets used by the Space, declared in the Space card.
465    pub datasets: Option<Vec<String>>,
466    /// Models used by the Space, declared in the Space card.
467    pub models: Option<Vec<String>>,
468    /// Resource-group information, when the repo belongs to one.
469    pub resource_group: Option<serde_json::Value>,
470    /// Total size of the repo on disk, in bytes.
471    pub used_storage: Option<u64>,
472}
473
474/// Metadata for a kernel repository on the Hub.
475///
476/// Returned by [`HFRepository::info`] when the repo is a kernel. The Hub's
477/// `/api/kernels/{repo_id}` endpoint returns a slim shape compared with
478/// model/dataset/space repos — fields like `tags`, `cardData`, and `siblings`
479/// are not exposed by this endpoint and are intentionally absent here. Most
480/// fields are optional because they depend on the repo's state.
481///
482/// Kernels are also retrievable via `/api/models/{repo_id}` (kernels carry
483/// `library_name: "kernels"`) if you need the full model-style metadata; in
484/// that case go through [`HFClient::model`] and the [`ModelInfo`] response.
485#[derive(Debug, Clone, Deserialize, Serialize)]
486#[serde(rename_all = "camelCase")]
487pub struct KernelInfo {
488    /// Repo ID, in the form `owner/name`.
489    pub id: String,
490    /// Internal Hub identifier (the API's `_id` field). Most callers should use `id` instead.
491    #[serde(rename = "_id")]
492    pub internal_id: Option<String>,
493    /// Owner of the repo (the part before `/` in `id`).
494    pub author: Option<String>,
495    /// Git commit SHA at the revision the response describes.
496    pub sha: Option<String>,
497    /// Whether the repo is private.
498    pub private: Option<bool>,
499    /// Gated-access state. Either the boolean `false` (open) or the string `"auto"`/`"manual"` indicating
500    /// the approval mode for access requests. Modeled as raw JSON because the field is union-typed.
501    pub gated: Option<serde_json::Value>,
502    /// Total downloads of this kernel.
503    pub downloads: Option<u64>,
504    /// Number of likes on the kernel.
505    pub likes: Option<u64>,
506    /// ISO-8601 timestamp of the most recent commit to the repo.
507    pub last_modified: Option<String>,
508    /// Whether the publisher is trusted by the Hub. Kernels from trusted publishers receive a
509    /// distinct badge in the UI.
510    pub trusted_publisher: Option<bool>,
511    /// Driver families the prebuilt kernel artifacts support, e.g., `"cuda"`, `"xpu"`, `"cpu"`.
512    /// May be absent when the kernel has not declared its supported drivers.
513    pub supported_driver_families: Option<Vec<String>>,
514}
515
516/// URL of a repository, returned by create/move endpoints.
517#[derive(Debug, Clone, Deserialize)]
518pub struct RepoUrl {
519    /// Absolute URL of the repository on the Hub.
520    pub url: String,
521}
522
523/// A handle for a single repository on the Hugging Face Hub, parameterized by the
524/// repo kind via the type-level marker `T`.
525///
526/// `HFRepository<T>` is created via the typed factories on [`HFClient`] —
527/// [`HFClient::model`], [`HFClient::dataset`], [`HFClient::space`], or
528/// [`HFClient::kernel`] — and binds together the client, owner, and repo name. The
529/// repo kind lives in the type system rather than in a runtime field, so methods
530/// that differ per repo kind (such as [`info`](HFRepository::info)) are dispatched
531/// at compile time and mismatches are impossible by construction.
532///
533/// Cheap to clone — the inner [`HFClient`] is `Arc`-backed.
534///
535/// # Example
536///
537/// ```rust,no_run
538/// # use hf_hub::HFClient;
539/// # #[tokio::main] async fn main() -> hf_hub::HFResult<()> {
540/// let client = HFClient::builder().build()?;
541/// let repo = client.model("openai-community", "gpt2");
542/// let info = repo.info().send().await?;
543/// # Ok(()) }
544/// ```
545pub struct HFRepository<T: RepoType> {
546    pub(crate) hf_client: HFClient,
547    pub(super) owner: String,
548    pub(super) name: String,
549    pub(super) repo_type: T,
550}
551
552impl<T: RepoType> Clone for HFRepository<T> {
553    fn clone(&self) -> Self {
554        Self {
555            hf_client: self.hf_client.clone(),
556            owner: self.owner.clone(),
557            name: self.name.clone(),
558            repo_type: self.repo_type,
559        }
560    }
561}
562
563impl Serialize for GatedApprovalMode {
564    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
565    where
566        S: Serializer,
567    {
568        match self {
569            GatedApprovalMode::Disabled => serializer.serialize_bool(false),
570            GatedApprovalMode::Auto => serializer.serialize_str("auto"),
571            GatedApprovalMode::Manual => serializer.serialize_str("manual"),
572        }
573    }
574}
575
576impl FromStr for GatedApprovalMode {
577    type Err = HFError;
578
579    fn from_str(s: &str) -> Result<Self, Self::Err> {
580        match s.to_lowercase().as_str() {
581            "false" | "disabled" => Ok(GatedApprovalMode::Disabled),
582            "auto" => Ok(GatedApprovalMode::Auto),
583            "manual" => Ok(GatedApprovalMode::Manual),
584            _ => Err(HFError::InvalidParameter(format!(
585                "unknown gated approval mode: {s:?}. Expected 'auto', 'manual', or 'false'"
586            ))),
587        }
588    }
589}
590
591impl FromStr for GatedNotificationsMode {
592    type Err = HFError;
593
594    fn from_str(s: &str) -> Result<Self, Self::Err> {
595        match s.to_lowercase().as_str() {
596            "bulk" => Ok(GatedNotificationsMode::Bulk),
597            "real-time" | "realtime" => Ok(GatedNotificationsMode::RealTime),
598            _ => Err(HFError::InvalidParameter(format!(
599                "unknown gated notifications mode: {s:?}. Expected 'bulk' or 'real-time'"
600            ))),
601        }
602    }
603}
604
605impl<T: RepoType> std::fmt::Debug for HFRepository<T> {
606    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
607        f.debug_struct("HFRepository")
608            .field("owner", &self.owner)
609            .field("name", &self.name)
610            .field("repo_type", &self.repo_type.singular())
611            .finish()
612    }
613}
614
615impl HFClient {
616    /// Create an [`HFRepository`] handle for any repo kind via a turbofished generic.
617    ///
618    /// Equivalent to the typed shortcuts ([`model`](Self::model), [`dataset`](Self::dataset),
619    /// [`space`](Self::space), [`kernel`](Self::kernel)) but useful when the kind is
620    /// expressed by an external type parameter or a `match` arm — for example, dispatching
621    /// from a CLI flag:
622    ///
623    /// ```rust,no_run
624    /// # use hf_hub::{HFClient, RepoTypeDataset};
625    /// # let client = HFClient::builder().build().unwrap();
626    /// let repo = client.repository(RepoTypeDataset, "rajpurkar", "squad");
627    /// # let _ = repo;
628    /// ```
629    pub fn repository<T: RepoType>(
630        &self,
631        repo_type: T,
632        owner: impl Into<String>,
633        name: impl Into<String>,
634    ) -> HFRepository<T> {
635        HFRepository::new(self.clone(), repo_type, owner, name)
636    }
637
638    /// Create an [`HFRepository`] handle for a model repository.
639    pub fn model(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeModel> {
640        self.repository(RepoTypeModel, owner, name)
641    }
642
643    /// Create an [`HFRepository`] handle for a dataset repository.
644    pub fn dataset(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeDataset> {
645        self.repository(RepoTypeDataset, owner, name)
646    }
647
648    /// Create an [`HFRepository`] handle for a Space repository.
649    pub fn space(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeSpace> {
650        self.repository(RepoTypeSpace, owner, name)
651    }
652
653    /// Create an [`HFRepository`] handle for a kernel repository.
654    pub fn kernel(&self, owner: impl Into<String>, name: impl Into<String>) -> HFRepository<RepoTypeKernel> {
655        self.repository(RepoTypeKernel, owner, name)
656    }
657}
658
659#[bon]
660impl HFClient {
661    /// List models on the Hub. Endpoint: `GET /api/models`.
662    ///
663    /// Returns a stream of [`ModelInfo`] entries. Pagination is automatic.
664    ///
665    /// # Parameters
666    ///
667    /// - `search`: free-text query forwarded as the `?search=` parameter. The Hub matches it substring-style against
668    ///   the model `id` and (when present) the model card description — it is **not** a tag filter.
669    /// - `author`: namespace owner to filter on, forwarded as `?author=`. Pass a Hub user or organization name (e.g.,
670    ///   `"google"`, `"meta-llama"`) — bare names, not paths.
671    /// - `filter`: a single Hub **tag** value forwarded as `?filter=`. Tags use the Hub's namespaced format, e.g.,
672    ///   `"pytorch"`, `"text-generation"`, `"license:apache-2.0"`, `"language:en"`, `"dataset:wikipedia"`,
673    ///   `"region:us"`. To combine tags, narrow the results client-side (only one `filter` value is sent).
674    /// - `sort`: API field name to sort by, forwarded as `?sort=`. Common values are `"downloads"`, `"likes"`,
675    ///   `"createdAt"`, `"lastModified"`, and `"trendingScore"`. Use the camelCase Hub field names (not Rust struct
676    ///   field names).
677    /// - `pipeline_tag`: pipeline-tag filter (e.g., `"text-classification"`, `"automatic-speech-recognition"`),
678    ///   forwarded as `?pipeline_tag=`. Same vocabulary as the `pipeline_tag` field on a model card.
679    /// - `full`: fetch the full model information including all fields.
680    /// - `card_data`: include the model card metadata in the response.
681    /// - `fetch_config`: include the model configuration in the response.
682    /// - `limit`: cap on the total number of items yielded by the stream. When less than 1000, also used as the server
683    ///   page size.
684    #[builder(finish_fn = send, derive(Debug, Clone))]
685    pub fn list_models(
686        &self,
687        /// Free-text query forwarded as the `?search=` parameter. The Hub matches it substring-style against
688        /// the model `id` and (when present) the model card description — it is **not** a tag filter.
689        #[builder(into)]
690        search: Option<String>,
691        /// Namespace owner to filter on, forwarded as `?author=`. Pass a Hub user or organization name (e.g.,
692        /// `"google"`, `"meta-llama"`) — bare names, not paths.
693        #[builder(into)]
694        author: Option<String>,
695        /// A single Hub **tag** value forwarded as `?filter=`. Tags use the Hub's namespaced format, e.g.,
696        /// `"pytorch"`, `"text-generation"`, `"license:apache-2.0"`, `"language:en"`, `"dataset:wikipedia"`,
697        /// `"region:us"`. To combine tags, narrow the results client-side (only one `filter` value is sent).
698        #[builder(into)]
699        filter: Option<String>,
700        /// API field name to sort by, forwarded as `?sort=`. Common values are `"downloads"`, `"likes"`,
701        /// `"createdAt"`, `"lastModified"`, and `"trendingScore"`. Use the camelCase Hub field names (not Rust struct
702        /// field names).
703        #[builder(into)]
704        sort: Option<String>,
705        /// Pipeline-tag filter (e.g., `"text-classification"`, `"automatic-speech-recognition"`),
706        /// forwarded as `?pipeline_tag=`. Same vocabulary as the `pipeline_tag` field on a model card.
707        #[builder(into)]
708        pipeline_tag: Option<String>,
709        /// Fetch the full model information including all fields.
710        full: Option<bool>,
711        /// Include the model card metadata in the response.
712        card_data: Option<bool>,
713        /// Include the model configuration in the response.
714        fetch_config: Option<bool>,
715        /// Cap on the total number of items yielded by the stream. When less than 1000, also used as the server
716        /// page size.
717        limit: Option<usize>,
718    ) -> HFResult<impl Stream<Item = HFResult<ModelInfo>> + '_> {
719        let url = Url::parse(&format!("{}/api/models", self.endpoint()))?;
720        let mut query: Vec<(String, String)> = Vec::new();
721        if let Some(ref s) = search {
722            query.push(("search".into(), s.clone()));
723        }
724        if let Some(ref a) = author {
725            query.push(("author".into(), a.clone()));
726        }
727        if let Some(ref f) = filter {
728            query.push(("filter".into(), f.clone()));
729        }
730        if let Some(ref s) = sort {
731            query.push(("sort".into(), s.clone()));
732        }
733        if let Some(max) = limit {
734            // The Hub API usually returns up to 1000 items per page by default,
735            // so only set an explicit limit for smaller requests.
736            if max < 1000 {
737                query.push(("limit".into(), max.to_string()));
738            }
739        }
740        if let Some(ref pt) = pipeline_tag {
741            query.push(("pipeline_tag".into(), pt.clone()));
742        }
743        if full == Some(true) {
744            query.push(("full".into(), "true".into()));
745        }
746        if card_data == Some(true) {
747            query.push(("cardData".into(), "true".into()));
748        }
749        if fetch_config == Some(true) {
750            query.push(("config".into(), "true".into()));
751        }
752        Ok(self.paginate(url, query, limit))
753    }
754
755    /// List datasets on the Hub. Endpoint: `GET /api/datasets`.
756    ///
757    /// Returns a stream of [`DatasetInfo`] entries. Pagination is automatic.
758    ///
759    /// # Parameters
760    ///
761    /// - `search`: free-text query forwarded as `?search=`. The Hub matches it substring-style against the dataset `id`
762    ///   and card description — not a tag filter.
763    /// - `author`: namespace owner forwarded as `?author=`. Pass a bare Hub user or organization name (e.g.,
764    ///   `"HuggingFaceH4"`, `"allenai"`).
765    /// - `filter`: a single Hub **tag** value forwarded as `?filter=`. Tags use the Hub's namespaced format, e.g.,
766    ///   `"task_categories:text-classification"`, `"language:en"`, `"size_categories:10K<n<100K"`, `"license:mit"`. To
767    ///   combine tags, narrow client-side — only one `filter` value is sent.
768    /// - `sort`: API field name to sort by, forwarded as `?sort=`. Common values are `"downloads"`, `"likes"`,
769    ///   `"createdAt"`, `"lastModified"`, and `"trendingScore"` (Hub camelCase field names).
770    /// - `full`: fetch the full dataset information including all fields.
771    /// - `limit`: cap on the total number of items yielded by the stream. When less than 1000, also used as the server
772    ///   page size.
773    #[builder(finish_fn = send, derive(Debug, Clone))]
774    pub fn list_datasets(
775        &self,
776        /// Free-text query forwarded as `?search=`. The Hub matches it substring-style against the dataset `id`
777        /// and card description — not a tag filter.
778        #[builder(into)]
779        search: Option<String>,
780        /// Namespace owner forwarded as `?author=`. Pass a bare Hub user or organization name (e.g.,
781        /// `"HuggingFaceH4"`, `"allenai"`).
782        #[builder(into)]
783        author: Option<String>,
784        /// A single Hub **tag** value forwarded as `?filter=`. Tags use the Hub's namespaced format, e.g.,
785        /// `"task_categories:text-classification"`, `"language:en"`, `"size_categories:10K<n<100K"`, `"license:mit"`.
786        /// To combine tags, narrow client-side — only one `filter` value is sent.
787        #[builder(into)]
788        filter: Option<String>,
789        /// API field name to sort by, forwarded as `?sort=`. Common values are `"downloads"`, `"likes"`,
790        /// `"createdAt"`, `"lastModified"`, and `"trendingScore"` (Hub camelCase field names).
791        #[builder(into)]
792        sort: Option<String>,
793        /// Fetch the full dataset information including all fields.
794        full: Option<bool>,
795        /// Cap on the total number of items yielded by the stream. When less than 1000, also used as the server
796        /// page size.
797        limit: Option<usize>,
798    ) -> HFResult<impl Stream<Item = HFResult<DatasetInfo>> + '_> {
799        let url = Url::parse(&format!("{}/api/datasets", self.endpoint()))?;
800        let mut query: Vec<(String, String)> = Vec::new();
801        if let Some(ref s) = search {
802            query.push(("search".into(), s.clone()));
803        }
804        if let Some(ref a) = author {
805            query.push(("author".into(), a.clone()));
806        }
807        if let Some(ref f) = filter {
808            query.push(("filter".into(), f.clone()));
809        }
810        if let Some(ref s) = sort {
811            query.push(("sort".into(), s.clone()));
812        }
813        if let Some(max) = limit
814            && max < 1000
815        {
816            query.push(("limit".into(), max.to_string()));
817        }
818        if full == Some(true) {
819            query.push(("full".into(), "true".into()));
820        }
821        Ok(self.paginate(url, query, limit))
822    }
823
824    /// List Spaces on the Hub. Endpoint: `GET /api/spaces`.
825    ///
826    /// Returns a stream of [`SpaceInfo`] entries. Pagination is automatic.
827    ///
828    /// # Parameters
829    ///
830    /// - `search`: free-text query forwarded as `?search=`. The Hub matches it substring-style against the Space `id`
831    ///   and card description — not a tag filter.
832    /// - `author`: namespace owner forwarded as `?author=`. Pass a bare Hub user or organization name (e.g.,
833    ///   `"openai"`, `"stabilityai"`).
834    /// - `filter`: a single Hub **tag** value forwarded as `?filter=`. Tags use the Hub's namespaced format, e.g.,
835    ///   `"sdk:gradio"`, `"sdk:streamlit"`, `"sdk:docker"`, `"language:en"`, `"license:mit"`. To combine tags, narrow
836    ///   client-side — only one `filter` value is sent.
837    /// - `sort`: API field name to sort by, forwarded as `?sort=`. Common values are `"likes"`, `"createdAt"`,
838    ///   `"lastModified"`, and `"trendingScore"` (Hub camelCase field names).
839    /// - `full`: fetch the full Space information including all fields.
840    /// - `limit`: cap on the total number of items yielded by the stream. When less than 1000, also used as the server
841    ///   page size.
842    #[builder(finish_fn = send, derive(Debug, Clone))]
843    pub fn list_spaces(
844        &self,
845        /// Free-text query forwarded as `?search=`. The Hub matches it substring-style against the Space `id`
846        /// and card description — not a tag filter.
847        #[builder(into)]
848        search: Option<String>,
849        /// Namespace owner forwarded as `?author=`. Pass a bare Hub user or organization name (e.g., `"openai"`,
850        /// `"stabilityai"`).
851        #[builder(into)]
852        author: Option<String>,
853        /// A single Hub **tag** value forwarded as `?filter=`. Tags use the Hub's namespaced format, e.g.,
854        /// `"sdk:gradio"`, `"sdk:streamlit"`, `"sdk:docker"`, `"language:en"`, `"license:mit"`. To combine tags,
855        /// narrow client-side — only one `filter` value is sent.
856        #[builder(into)]
857        filter: Option<String>,
858        /// API field name to sort by, forwarded as `?sort=`. Common values are `"likes"`, `"createdAt"`,
859        /// `"lastModified"`, and `"trendingScore"` (Hub camelCase field names).
860        #[builder(into)]
861        sort: Option<String>,
862        /// Fetch the full Space information including all fields.
863        full: Option<bool>,
864        /// Cap on the total number of items yielded by the stream. When less than 1000, also used as the server
865        /// page size.
866        limit: Option<usize>,
867    ) -> HFResult<impl Stream<Item = HFResult<SpaceInfo>> + '_> {
868        let url = Url::parse(&format!("{}/api/spaces", self.endpoint()))?;
869        let mut query: Vec<(String, String)> = Vec::new();
870        if let Some(ref s) = search {
871            query.push(("search".into(), s.clone()));
872        }
873        if let Some(ref a) = author {
874            query.push(("author".into(), a.clone()));
875        }
876        if let Some(ref f) = filter {
877            query.push(("filter".into(), f.clone()));
878        }
879        if let Some(ref s) = sort {
880            query.push(("sort".into(), s.clone()));
881        }
882        if let Some(max) = limit
883            && max < 1000
884        {
885            query.push(("limit".into(), max.to_string()));
886        }
887        if full == Some(true) {
888            query.push(("full".into(), "true".into()));
889        }
890        Ok(self.paginate(url, query, limit))
891    }
892
893    /// Create a new repository. Endpoint: `POST /api/repos/create`.
894    ///
895    /// The repo kind is picked at the call site by passing one of the four marker
896    /// structs to [`repo_type`](HFClientCreateRepositoryBuilder::repo_type) — e.g.,
897    /// `client.create_repository().repo_type(RepoTypeDataset)...` to create a dataset.
898    /// The body always includes the `type` field, matching the Hub's per-kind
899    /// defaults.
900    ///
901    /// # Parameters
902    ///
903    /// - `repo_id` (required): repository ID in `"owner/name"` or `"name"` format.
904    /// - `repo_type` (required): the repo kind ([`RepoTypeModel`], [`RepoTypeDataset`], [`RepoTypeSpace`], or
905    ///   [`RepoTypeKernel`]).
906    /// - `private`: whether the repository should be private.
907    /// - `exist_ok` (default `false`): if `true`, do not error when the repository already exists.
908    /// - `space_sdk`: SDK for a Space (e.g., `"gradio"`, `"streamlit"`, `"docker"`). Required when `repo_type` is
909    ///   [`RepoTypeSpace`]; ignored for other repo kinds.
910    #[builder(finish_fn = send, derive(Debug, Clone))]
911    pub async fn create_repository(
912        &self,
913        /// Repository ID in `"owner/name"` or `"name"` format.
914        repo_id: &str,
915        /// The repo kind ([`RepoTypeModel`], [`RepoTypeDataset`], [`RepoTypeSpace`],
916        /// or [`RepoTypeKernel`]).
917        repo_type: impl RepoType,
918        /// Whether the repository should be private.
919        private: Option<bool>,
920        /// If `true`, do not error when the repository already exists.
921        #[builder(default)]
922        exist_ok: bool,
923        /// SDK for a Space (e.g., `"gradio"`, `"streamlit"`, `"docker"`). Required when `repo_type` is
924        /// [`RepoTypeSpace`]; ignored for other repo kinds.
925        #[builder(into)]
926        space_sdk: Option<String>,
927    ) -> HFResult<RepoUrl> {
928        let url = format!("{}/api/repos/create", self.endpoint());
929
930        let (namespace, name) = split_repo_id(repo_id);
931
932        let mut body = serde_json::json!({
933            "name": name,
934            "private": private.unwrap_or(false),
935            "type": repo_type.singular(),
936        });
937
938        if let Some(ns) = namespace {
939            body["organization"] = serde_json::Value::String(ns.to_string());
940        }
941        if let Some(sdk) = space_sdk {
942            body["sdk"] = serde_json::Value::String(sdk);
943        }
944
945        let headers = self.auth_headers();
946        let response = retry::retry(self.retry_config(), || {
947            self.http_client().post(&url).headers(headers.clone()).json(&body).send()
948        })
949        .await?;
950
951        if response.status().as_u16() == 409 && exist_ok {
952            return Ok(RepoUrl {
953                url: format!("{}/{}{}", self.endpoint(), repo_type.url_prefix(), repo_id),
954            });
955        }
956
957        let response = self
958            .check_response(response, None, crate::error::NotFoundContext::Generic)
959            .await?;
960        Ok(response.json().await?)
961    }
962
963    /// Delete a repository. Endpoint: `DELETE /api/repos/delete`.
964    ///
965    /// The repo kind is picked at the call site by passing one of the four marker
966    /// structs to [`repo_type`](HFClientDeleteRepositoryBuilder::repo_type) — e.g.,
967    /// `client.delete_repository().repo_type(RepoTypeSpace)...`. The body always includes
968    /// the `type` field.
969    ///
970    /// # Parameters
971    ///
972    /// - `repo_id` (required): repository ID in `"owner/name"` or `"name"` format.
973    /// - `repo_type` (required): the repo kind ([`RepoTypeModel`], [`RepoTypeDataset`], [`RepoTypeSpace`], or
974    ///   [`RepoTypeKernel`]).
975    /// - `missing_ok` (default `false`): if `true`, do not error when the repository does not exist.
976    #[builder(finish_fn = send, derive(Debug, Clone))]
977    pub async fn delete_repository(
978        &self,
979        /// Repository ID in `"owner/name"` or `"name"` format.
980        repo_id: &str,
981        /// The repo kind ([`RepoTypeModel`], [`RepoTypeDataset`], [`RepoTypeSpace`],
982        /// or [`RepoTypeKernel`]).
983        repo_type: impl RepoType,
984        /// If `true`, do not error when the repository does not exist.
985        #[builder(default)]
986        missing_ok: bool,
987    ) -> HFResult<()> {
988        let url = format!("{}/api/repos/delete", self.endpoint());
989
990        let (namespace, name) = split_repo_id(repo_id);
991
992        let mut body = serde_json::json!({
993            "name": name,
994            "type": repo_type.singular(),
995        });
996        if let Some(ns) = namespace {
997            body["organization"] = serde_json::Value::String(ns.to_string());
998        }
999
1000        let headers = self.auth_headers();
1001        let response = retry::retry(self.retry_config(), || {
1002            self.http_client().delete(&url).headers(headers.clone()).json(&body).send()
1003        })
1004        .await?;
1005
1006        if response.status().as_u16() == 404 && missing_ok {
1007            return Ok(());
1008        }
1009
1010        self.check_response(response, Some(repo_id), crate::error::NotFoundContext::Repo)
1011            .await?;
1012        Ok(())
1013    }
1014
1015    /// Move (rename) a repository. Endpoint: `POST /api/repos/move`.
1016    ///
1017    /// The repo kind is picked at the call site by passing one of the four marker
1018    /// structs to [`repo_type`](HFClientMoveRepositoryBuilder::repo_type) — e.g.,
1019    /// `client.move_repository().repo_type(RepoTypeModel)...`. The body always includes
1020    /// the `type` field.
1021    ///
1022    /// # Parameters
1023    ///
1024    /// - `from_id` (required): current repository ID in `"owner/name"` format.
1025    /// - `to_id` (required): new repository ID in `"owner/name"` format.
1026    /// - `repo_type` (required): the repo kind ([`RepoTypeModel`], [`RepoTypeDataset`], [`RepoTypeSpace`], or
1027    ///   [`RepoTypeKernel`]).
1028    #[builder(finish_fn = send, derive(Debug, Clone))]
1029    pub async fn move_repository(
1030        &self,
1031        /// Current repository ID in `"owner/name"` format.
1032        from_id: &str,
1033        /// New repository ID in `"owner/name"` format.
1034        to_id: &str,
1035        /// The repo kind ([`RepoTypeModel`], [`RepoTypeDataset`], [`RepoTypeSpace`],
1036        /// or [`RepoTypeKernel`]).
1037        repo_type: impl RepoType,
1038    ) -> HFResult<RepoUrl> {
1039        let url = format!("{}/api/repos/move", self.endpoint());
1040        let body = serde_json::json!({
1041            "fromRepo": from_id,
1042            "toRepo": to_id,
1043            "type": repo_type.singular(),
1044        });
1045
1046        let headers = self.auth_headers();
1047        let response = retry::retry(self.retry_config(), || {
1048            self.http_client().post(&url).headers(headers.clone()).json(&body).send()
1049        })
1050        .await?;
1051
1052        self.check_response(response, None, crate::error::NotFoundContext::Generic)
1053            .await?;
1054        Ok(RepoUrl {
1055            url: format!("{}/{}{}", self.endpoint(), repo_type.url_prefix(), to_id),
1056        })
1057    }
1058}
1059
1060impl<T: RepoType> HFRepository<T> {
1061    /// Construct a new repository handle. Prefer the factory methods on [`HFClient`] instead.
1062    pub fn new(client: HFClient, repo_type: T, owner: impl Into<String>, name: impl Into<String>) -> Self {
1063        Self {
1064            hf_client: client,
1065            owner: owner.into(),
1066            name: name.into(),
1067            repo_type,
1068        }
1069    }
1070
1071    /// Return a reference to the underlying [`HFClient`].
1072    pub fn client(&self) -> &HFClient {
1073        &self.hf_client
1074    }
1075
1076    /// The repository owner (user or organization name).
1077    pub fn owner(&self) -> &str {
1078        &self.owner
1079    }
1080
1081    /// The repository name (without owner prefix).
1082    pub fn name(&self) -> &str {
1083        &self.name
1084    }
1085
1086    /// The full `"owner/name"` identifier used in Hub API calls.
1087    ///
1088    /// If no owner is set, returns just the name (for repos using short-form IDs like `"gpt2"`).
1089    pub fn repo_path(&self) -> String {
1090        if self.owner.is_empty() {
1091            self.name.clone()
1092        } else {
1093            format!("{}/{}", self.owner, self.name)
1094        }
1095    }
1096
1097    /// The marker for this handle's repo kind.
1098    ///
1099    /// Returns a reference to the stored marker value. Call
1100    /// [`singular`](RepoType::singular), [`plural`](RepoType::plural), or
1101    /// [`url_prefix`](RepoType::url_prefix) on it to get the corresponding string.
1102    /// For `T = RepoTypeAny`, the returned value reflects the runtime variant the
1103    /// handle was constructed with.
1104    pub fn repo_type(&self) -> &T {
1105        &self.repo_type
1106    }
1107
1108    /// Fetch repo info for this repository's kind, deserializing into `I`.
1109    /// Endpoint: `GET /api/{plural}/{repo_id}[/revision/{revision}]`.
1110    pub(crate) async fn fetch_repo_info<I: DeserializeOwned>(
1111        &self,
1112        revision: Option<String>,
1113        expand: Option<Vec<String>>,
1114    ) -> HFResult<I> {
1115        let mut url = self.hf_client.api_url(self.repo_type.plural(), &self.repo_path());
1116        if let Some(ref revision) = revision {
1117            url = format!("{url}/revision/{}", crate::client::encode_ref(revision));
1118        }
1119        let headers = self.hf_client.auth_headers();
1120        let expand_params: Option<Vec<(&str, &str)>> =
1121            expand.as_ref().map(|e| e.iter().map(|v| ("expand", v.as_str())).collect());
1122        let response = retry::retry(self.hf_client.retry_config(), || {
1123            let mut req = self.hf_client.http_client().get(&url).headers(headers.clone());
1124            if let Some(ref params) = expand_params {
1125                req = req.query(params);
1126            }
1127            req.send()
1128        })
1129        .await?;
1130        let repo_path = self.repo_path();
1131        let not_found_ctx = match revision {
1132            Some(rev) => crate::error::NotFoundContext::Revision { revision: rev },
1133            None => crate::error::NotFoundContext::Repo,
1134        };
1135        let response = self.hf_client.check_response(response, Some(&repo_path), not_found_ctx).await?;
1136        Ok(response.json().await?)
1137    }
1138}
1139
1140#[bon]
1141impl<T: RepoType> HFRepository<T> {
1142    /// Return `true` if the repository exists.
1143    ///
1144    /// Returns `Ok(false)` only when the Hub responds with 404. If the repo exists but the current
1145    /// credentials don't have access (private/gated), this returns an error
1146    /// ([`HFError::AuthRequired`] or [`HFError::Forbidden`]), not `Ok(false)`.
1147    #[builder(finish_fn = send, derive(Debug, Clone))]
1148    pub async fn exists(&self) -> HFResult<bool> {
1149        let url = self.hf_client.api_url(self.repo_type.plural(), &self.repo_path());
1150        let headers = self.hf_client.auth_headers();
1151        let response = retry::retry(self.hf_client.retry_config(), || {
1152            self.hf_client.http_client().get(&url).headers(headers.clone()).send()
1153        })
1154        .await?;
1155        if response.status() == reqwest::StatusCode::NOT_FOUND {
1156            return Ok(false);
1157        }
1158        self.hf_client
1159            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
1160            .await?;
1161        Ok(true)
1162    }
1163
1164    /// Return `true` if the given revision (branch, tag, or commit SHA) exists.
1165    ///
1166    /// Returns `Ok(false)` only when the Hub responds with 404. If the repo exists but the current
1167    /// credentials don't have access (private/gated), this returns an error
1168    /// ([`HFError::AuthRequired`] or [`HFError::Forbidden`]), not `Ok(false)`.
1169    ///
1170    /// # Parameters
1171    ///
1172    /// - `revision` (required): Git revision to check for existence.
1173    #[builder(finish_fn = send, derive(Debug, Clone))]
1174    pub async fn revision_exists(
1175        &self,
1176        /// Git revision to check for existence.
1177        revision: &str,
1178    ) -> HFResult<bool> {
1179        let url = format!(
1180            "{}/revision/{}",
1181            self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()),
1182            crate::client::encode_ref(revision)
1183        );
1184        let headers = self.hf_client.auth_headers();
1185        let response = retry::retry(self.hf_client.retry_config(), || {
1186            self.hf_client.http_client().get(&url).headers(headers.clone()).send()
1187        })
1188        .await?;
1189        if response.status() == reqwest::StatusCode::NOT_FOUND {
1190            return Ok(false);
1191        }
1192        self.hf_client
1193            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
1194            .await?;
1195        Ok(true)
1196    }
1197
1198    /// Return `true` if the given file exists in the repository at the specified revision.
1199    ///
1200    /// # Parameters
1201    ///
1202    /// - `filename` (required): path of the file to check within the repository.
1203    /// - `revision`: Git revision to check. Defaults to the main branch.
1204    #[builder(finish_fn = send, derive(Debug, Clone))]
1205    pub async fn file_exists(
1206        &self,
1207        /// Path of the file to check within the repository.
1208        filename: &str,
1209        /// Git revision to check. Defaults to the main branch.
1210        revision: Option<&str>,
1211    ) -> HFResult<bool> {
1212        let revision = revision.unwrap_or(constants::DEFAULT_REVISION);
1213        let url = self
1214            .hf_client
1215            .download_url(self.repo_type.url_prefix(), &self.repo_path(), revision, filename)?;
1216        let headers = self.hf_client.auth_headers();
1217        let response = retry::retry(self.hf_client.retry_config(), || {
1218            self.hf_client.http_client().head(&url).headers(headers.clone()).send()
1219        })
1220        .await?;
1221        if response.status() == reqwest::StatusCode::NOT_FOUND {
1222            if self.revision_exists().revision(revision).send().await? {
1223                return Ok(false);
1224            }
1225            return Err(HFError::RevisionNotFound {
1226                repo_id: self.repo_path(),
1227                revision: revision.to_string(),
1228                context: None,
1229            });
1230        }
1231        self.hf_client
1232            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Generic)
1233            .await?;
1234        Ok(true)
1235    }
1236
1237    /// Update repository settings such as visibility, gating policy, description,
1238    /// discussion settings, and gated notification preferences.
1239    ///
1240    /// Endpoint: `PUT /api/{plural}/{repo_id}/settings`.
1241    ///
1242    /// # Parameters
1243    ///
1244    /// - `private`: whether the repository should be private.
1245    /// - `gated`: access-gating mode for the repository (e.g., `auto`, `manual`, disabled).
1246    /// - `description`: repository description shown on the Hub page.
1247    /// - `discussions_disabled`: whether discussions are disabled on this repository.
1248    /// - `gated_notifications`: notification cadence (and optional email override) for gated-access requests. The
1249    ///   cadence is required when this is set; pass [`GatedNotifications::with_email`] to override the recipient too.
1250    #[builder(finish_fn = send, derive(Debug, Clone))]
1251    pub async fn update_settings(
1252        &self,
1253        /// Whether the repository should be private.
1254        private: Option<bool>,
1255        /// Access-gating mode for the repository (e.g., `auto`, `manual`, disabled).
1256        gated: Option<GatedApprovalMode>,
1257        /// Repository description shown on the Hub page.
1258        #[builder(into)]
1259        description: Option<String>,
1260        /// Whether discussions are disabled on this repository.
1261        discussions_disabled: Option<bool>,
1262        /// Notification cadence (and optional email override) for gated-access requests.
1263        gated_notifications: Option<GatedNotifications>,
1264    ) -> HFResult<()> {
1265        #[derive(Serialize)]
1266        #[serde(rename_all = "camelCase")]
1267        struct UpdateSettingsBody<'a> {
1268            #[serde(skip_serializing_if = "Option::is_none")]
1269            private: Option<bool>,
1270            #[serde(skip_serializing_if = "Option::is_none")]
1271            gated: Option<&'a GatedApprovalMode>,
1272            #[serde(skip_serializing_if = "Option::is_none")]
1273            description: Option<&'a str>,
1274            #[serde(skip_serializing_if = "Option::is_none")]
1275            discussions_disabled: Option<bool>,
1276            #[serde(skip_serializing_if = "Option::is_none")]
1277            gated_notifications_email: Option<&'a str>,
1278            #[serde(skip_serializing_if = "Option::is_none")]
1279            gated_notifications_mode: Option<&'a GatedNotificationsMode>,
1280        }
1281
1282        let body = UpdateSettingsBody {
1283            private,
1284            gated: gated.as_ref(),
1285            description: description.as_deref(),
1286            discussions_disabled,
1287            gated_notifications_email: gated_notifications.as_ref().and_then(|g| g.email.as_deref()),
1288            gated_notifications_mode: gated_notifications.as_ref().map(|g| &g.mode),
1289        };
1290
1291        let url = format!("{}/settings", self.hf_client.api_url(self.repo_type.plural(), &self.repo_path()));
1292        let headers = self.hf_client.auth_headers();
1293
1294        let response = retry::retry(self.hf_client.retry_config(), || {
1295            self.hf_client
1296                .http_client()
1297                .put(&url)
1298                .headers(headers.clone())
1299                .json(&body)
1300                .send()
1301        })
1302        .await?;
1303
1304        let repo_path = self.repo_path();
1305        self.hf_client
1306            .check_response(response, Some(&repo_path), crate::error::NotFoundContext::Repo)
1307            .await?;
1308        Ok(())
1309    }
1310}
1311
1312/// Documentation shared by the per-repo-kind `info` builders.
1313macro_rules! info_method_doc {
1314    () => {
1315        "Fetch repository metadata for this repo kind, returning the concrete info struct.\n\n\
1316        # Parameters\n\n\
1317        - `revision`: Git revision (branch, tag, or commit SHA). Defaults to the main branch.\n\
1318        - `expand`: list of properties to expand in the response (e.g., `\"trendingScore\"`, `\"cardData\"`).\n  \
1319          When set, only the listed properties (plus `_id` and `id`) are returned. Kernel info ignores this."
1320    };
1321}
1322
1323#[bon]
1324impl HFRepository<RepoTypeModel> {
1325    /// Fetch [`ModelInfo`] for this model repository.
1326    #[doc = info_method_doc!()]
1327    #[builder(
1328        finish_fn = send,
1329        builder_type = HFModelInfoBuilder,
1330        state_mod(name = hf_model_info_builder, vis = "pub(crate)"),
1331        derive(Debug, Clone),
1332    )]
1333    pub async fn info(
1334        &self,
1335        /// Git revision (branch, tag, or commit SHA). Defaults to the main branch.
1336        #[builder(into)]
1337        revision: Option<String>,
1338        /// List of properties to expand in the response (e.g., `"trendingScore"`, `"cardData"`).
1339        expand: Option<Vec<String>>,
1340    ) -> HFResult<ModelInfo> {
1341        self.fetch_repo_info(revision, expand).await
1342    }
1343}
1344
1345#[bon]
1346impl HFRepository<RepoTypeDataset> {
1347    /// Fetch [`DatasetInfo`] for this dataset repository.
1348    #[doc = info_method_doc!()]
1349    #[builder(
1350        finish_fn = send,
1351        builder_type = HFDatasetInfoBuilder,
1352        state_mod(name = hf_dataset_info_builder, vis = "pub(crate)"),
1353        derive(Debug, Clone),
1354    )]
1355    pub async fn info(
1356        &self,
1357        /// Git revision (branch, tag, or commit SHA). Defaults to the main branch.
1358        #[builder(into)]
1359        revision: Option<String>,
1360        /// List of properties to expand in the response (e.g., `"trendingScore"`, `"cardData"`).
1361        expand: Option<Vec<String>>,
1362    ) -> HFResult<DatasetInfo> {
1363        self.fetch_repo_info(revision, expand).await
1364    }
1365}
1366
1367#[bon]
1368impl HFRepository<RepoTypeSpace> {
1369    /// Fetch [`SpaceInfo`] for this Space repository.
1370    #[doc = info_method_doc!()]
1371    #[builder(
1372        finish_fn = send,
1373        builder_type = HFSpaceInfoBuilder,
1374        state_mod(name = hf_space_info_builder, vis = "pub(crate)"),
1375        derive(Debug, Clone),
1376    )]
1377    pub async fn info(
1378        &self,
1379        /// Git revision (branch, tag, or commit SHA). Defaults to the main branch.
1380        #[builder(into)]
1381        revision: Option<String>,
1382        /// List of properties to expand in the response (e.g., `"trendingScore"`, `"cardData"`).
1383        expand: Option<Vec<String>>,
1384    ) -> HFResult<SpaceInfo> {
1385        self.fetch_repo_info(revision, expand).await
1386    }
1387}
1388
1389#[bon]
1390impl HFRepository<RepoTypeKernel> {
1391    /// Fetch [`KernelInfo`] for this kernel repository.
1392    ///
1393    /// Note: the Hub's `/api/kernels/{repo_id}` endpoint returns a slim shape (no `tags`,
1394    /// `cardData`, or `siblings`) and silently ignores `expand`. To get the full
1395    /// model-style metadata for a kernel, build a model handle for the same repo id
1396    /// (`client.model(owner, name)`) and call `info()` on it.
1397    #[doc = info_method_doc!()]
1398    #[builder(
1399        finish_fn = send,
1400        builder_type = HFKernelInfoBuilder,
1401        state_mod(name = hf_kernel_info_builder, vis = "pub(crate)"),
1402        derive(Debug, Clone),
1403    )]
1404    pub async fn info(
1405        &self,
1406        /// Git revision (branch, tag, or commit SHA). Defaults to the main branch.
1407        #[builder(into)]
1408        revision: Option<String>,
1409        /// List of properties to expand in the response. Kernel info ignores this parameter.
1410        expand: Option<Vec<String>>,
1411    ) -> HFResult<KernelInfo> {
1412        self.fetch_repo_info(revision, expand).await
1413    }
1414}
1415
1416/// Split "namespace/name" into (Some("namespace"), "name") or (None, "name")
1417fn split_repo_id(repo_id: &str) -> (Option<&str>, &str) {
1418    match repo_id.split_once('/') {
1419        Some((ns, name)) => (Some(ns), name),
1420        None => (None, repo_id),
1421    }
1422}
1423
1424#[cfg(feature = "blocking")]
1425#[bon]
1426impl crate::blocking::HFClientSync {
1427    /// Blocking counterpart of [`HFClient::list_models`]. Returns the collected stream as a
1428    /// `Vec<ModelInfo>`. See the async method for parameters and behavior.
1429    #[builder(finish_fn = send, derive(Debug, Clone))]
1430    pub fn list_models(
1431        &self,
1432        #[builder(into)] search: Option<String>,
1433        #[builder(into)] author: Option<String>,
1434        #[builder(into)] filter: Option<String>,
1435        #[builder(into)] sort: Option<String>,
1436        #[builder(into)] pipeline_tag: Option<String>,
1437        full: Option<bool>,
1438        card_data: Option<bool>,
1439        fetch_config: Option<bool>,
1440        limit: Option<usize>,
1441    ) -> HFResult<Vec<ModelInfo>> {
1442        use futures::StreamExt;
1443        self.runtime.block_on(async move {
1444            let stream = self
1445                .inner
1446                .list_models()
1447                .maybe_search(search)
1448                .maybe_author(author)
1449                .maybe_filter(filter)
1450                .maybe_sort(sort)
1451                .maybe_pipeline_tag(pipeline_tag)
1452                .maybe_full(full)
1453                .maybe_card_data(card_data)
1454                .maybe_fetch_config(fetch_config)
1455                .maybe_limit(limit)
1456                .send()?;
1457            futures::pin_mut!(stream);
1458            let mut items = Vec::new();
1459            while let Some(item) = stream.next().await {
1460                items.push(item?);
1461            }
1462            Ok(items)
1463        })
1464    }
1465
1466    /// Blocking counterpart of [`HFClient::list_datasets`]. Returns the collected stream as a
1467    /// `Vec<DatasetInfo>`. See the async method for parameters and behavior.
1468    #[builder(finish_fn = send, derive(Debug, Clone))]
1469    pub fn list_datasets(
1470        &self,
1471        #[builder(into)] search: Option<String>,
1472        #[builder(into)] author: Option<String>,
1473        #[builder(into)] filter: Option<String>,
1474        #[builder(into)] sort: Option<String>,
1475        full: Option<bool>,
1476        limit: Option<usize>,
1477    ) -> HFResult<Vec<DatasetInfo>> {
1478        use futures::StreamExt;
1479        self.runtime.block_on(async move {
1480            let stream = self
1481                .inner
1482                .list_datasets()
1483                .maybe_search(search)
1484                .maybe_author(author)
1485                .maybe_filter(filter)
1486                .maybe_sort(sort)
1487                .maybe_full(full)
1488                .maybe_limit(limit)
1489                .send()?;
1490            futures::pin_mut!(stream);
1491            let mut items = Vec::new();
1492            while let Some(item) = stream.next().await {
1493                items.push(item?);
1494            }
1495            Ok(items)
1496        })
1497    }
1498
1499    /// Blocking counterpart of [`HFClient::list_spaces`]. Returns the collected stream as a
1500    /// `Vec<SpaceInfo>`. See the async method for parameters and behavior.
1501    #[builder(finish_fn = send, derive(Debug, Clone))]
1502    pub fn list_spaces(
1503        &self,
1504        #[builder(into)] search: Option<String>,
1505        #[builder(into)] author: Option<String>,
1506        #[builder(into)] filter: Option<String>,
1507        #[builder(into)] sort: Option<String>,
1508        full: Option<bool>,
1509        limit: Option<usize>,
1510    ) -> HFResult<Vec<SpaceInfo>> {
1511        use futures::StreamExt;
1512        self.runtime.block_on(async move {
1513            let stream = self
1514                .inner
1515                .list_spaces()
1516                .maybe_search(search)
1517                .maybe_author(author)
1518                .maybe_filter(filter)
1519                .maybe_sort(sort)
1520                .maybe_full(full)
1521                .maybe_limit(limit)
1522                .send()?;
1523            futures::pin_mut!(stream);
1524            let mut items = Vec::new();
1525            while let Some(item) = stream.next().await {
1526                items.push(item?);
1527            }
1528            Ok(items)
1529        })
1530    }
1531
1532    /// Blocking counterpart of [`HFClient::create_repository`]. See the async method for parameters and
1533    /// behavior.
1534    #[builder(finish_fn = send, derive(Debug, Clone))]
1535    pub fn create_repository(
1536        &self,
1537        repo_id: &str,
1538        repo_type: impl RepoType,
1539        private: Option<bool>,
1540        #[builder(default)] exist_ok: bool,
1541        #[builder(into)] space_sdk: Option<String>,
1542    ) -> HFResult<RepoUrl> {
1543        self.runtime.block_on(
1544            self.inner
1545                .create_repository()
1546                .repo_id(repo_id)
1547                .repo_type(repo_type)
1548                .maybe_private(private)
1549                .exist_ok(exist_ok)
1550                .maybe_space_sdk(space_sdk)
1551                .send(),
1552        )
1553    }
1554
1555    /// Blocking counterpart of [`HFClient::delete_repository`]. See the async method for parameters and
1556    /// behavior.
1557    #[builder(finish_fn = send, derive(Debug, Clone))]
1558    pub fn delete_repository(
1559        &self,
1560        repo_id: &str,
1561        repo_type: impl RepoType,
1562        #[builder(default)] missing_ok: bool,
1563    ) -> HFResult<()> {
1564        self.runtime.block_on(
1565            self.inner
1566                .delete_repository()
1567                .repo_id(repo_id)
1568                .repo_type(repo_type)
1569                .missing_ok(missing_ok)
1570                .send(),
1571        )
1572    }
1573
1574    /// Blocking counterpart of [`HFClient::move_repository`]. See the async method for parameters and
1575    /// behavior.
1576    #[builder(finish_fn = send, derive(Debug, Clone))]
1577    pub fn move_repository(&self, from_id: &str, to_id: &str, repo_type: impl RepoType) -> HFResult<RepoUrl> {
1578        self.runtime.block_on(
1579            self.inner
1580                .move_repository()
1581                .from_id(from_id)
1582                .to_id(to_id)
1583                .repo_type(repo_type)
1584                .send(),
1585        )
1586    }
1587}
1588
1589#[cfg(feature = "blocking")]
1590#[bon]
1591impl<T: RepoType> crate::blocking::HFRepositorySync<T> {
1592    /// Blocking counterpart of [`HFRepository::exists`].
1593    #[builder(finish_fn = send, derive(Debug, Clone))]
1594    pub fn exists(&self) -> HFResult<bool> {
1595        self.runtime.block_on(self.inner.exists().send())
1596    }
1597
1598    /// Blocking counterpart of [`HFRepository::revision_exists`]. See the async method for
1599    /// parameters and behavior.
1600    #[builder(finish_fn = send, derive(Debug, Clone))]
1601    pub fn revision_exists(&self, revision: &str) -> HFResult<bool> {
1602        self.runtime.block_on(self.inner.revision_exists().revision(revision).send())
1603    }
1604
1605    /// Blocking counterpart of [`HFRepository::file_exists`]. See the async method for parameters
1606    /// and behavior.
1607    #[builder(finish_fn = send, derive(Debug, Clone))]
1608    pub fn file_exists(&self, filename: &str, revision: Option<&str>) -> HFResult<bool> {
1609        self.runtime
1610            .block_on(self.inner.file_exists().filename(filename).maybe_revision(revision).send())
1611    }
1612
1613    /// Blocking counterpart of [`HFRepository::update_settings`]. See the async method for
1614    /// parameters and behavior.
1615    #[builder(finish_fn = send, derive(Debug, Clone))]
1616    pub fn update_settings(
1617        &self,
1618        private: Option<bool>,
1619        gated: Option<GatedApprovalMode>,
1620        #[builder(into)] description: Option<String>,
1621        discussions_disabled: Option<bool>,
1622        gated_notifications: Option<GatedNotifications>,
1623    ) -> HFResult<()> {
1624        self.runtime.block_on(
1625            self.inner
1626                .update_settings()
1627                .maybe_private(private)
1628                .maybe_gated(gated)
1629                .maybe_description(description)
1630                .maybe_discussions_disabled(discussions_disabled)
1631                .maybe_gated_notifications(gated_notifications)
1632                .send(),
1633        )
1634    }
1635}
1636
1637/// Sync `info()` builders for each repo kind.
1638///
1639/// Each block mirrors the async per-kind `info()` impl in shape and forwards through the
1640/// runtime. Builder type names are unique to keep the bon-generated state types from
1641/// colliding across the four impl blocks.
1642
1643#[cfg(feature = "blocking")]
1644#[bon]
1645impl crate::blocking::HFRepositorySync<RepoTypeModel> {
1646    /// Blocking counterpart of [`HFRepository::<RepoTypeModel>::info`].
1647    #[builder(
1648        finish_fn = send,
1649        builder_type = HFModelInfoSyncBuilder,
1650        state_mod(name = hf_model_info_sync_builder, vis = "pub(crate)"),
1651        derive(Debug, Clone),
1652    )]
1653    pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<ModelInfo> {
1654        self.runtime
1655            .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1656    }
1657}
1658
1659#[cfg(feature = "blocking")]
1660#[bon]
1661impl crate::blocking::HFRepositorySync<RepoTypeDataset> {
1662    /// Blocking counterpart of [`HFRepository::<RepoTypeDataset>::info`].
1663    #[builder(
1664        finish_fn = send,
1665        builder_type = HFDatasetInfoSyncBuilder,
1666        state_mod(name = hf_dataset_info_sync_builder, vis = "pub(crate)"),
1667        derive(Debug, Clone),
1668    )]
1669    pub fn info(
1670        &self,
1671        #[builder(into)] revision: Option<String>,
1672        expand: Option<Vec<String>>,
1673    ) -> HFResult<DatasetInfo> {
1674        self.runtime
1675            .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1676    }
1677}
1678
1679#[cfg(feature = "blocking")]
1680#[bon]
1681impl crate::blocking::HFRepositorySync<RepoTypeSpace> {
1682    /// Blocking counterpart of [`HFRepository::<RepoTypeSpace>::info`].
1683    #[builder(
1684        finish_fn = send,
1685        builder_type = HFSpaceInfoSyncBuilder,
1686        state_mod(name = hf_space_info_sync_builder, vis = "pub(crate)"),
1687        derive(Debug, Clone),
1688    )]
1689    pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<SpaceInfo> {
1690        self.runtime
1691            .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1692    }
1693}
1694
1695#[cfg(feature = "blocking")]
1696#[bon]
1697impl crate::blocking::HFRepositorySync<RepoTypeKernel> {
1698    /// Blocking counterpart of [`HFRepository::<RepoTypeKernel>::info`].
1699    #[builder(
1700        finish_fn = send,
1701        builder_type = HFKernelInfoSyncBuilder,
1702        state_mod(name = hf_kernel_info_sync_builder, vis = "pub(crate)"),
1703        derive(Debug, Clone),
1704    )]
1705    pub fn info(&self, #[builder(into)] revision: Option<String>, expand: Option<Vec<String>>) -> HFResult<KernelInfo> {
1706        self.runtime
1707            .block_on(self.inner.info().maybe_revision(revision).maybe_expand(expand).send())
1708    }
1709}
1710
1711#[cfg(test)]
1712mod tests {
1713    use futures::StreamExt;
1714
1715    use super::{
1716        DatasetInfo, EvalResultEntry, HFRepository, InferenceProviderMapping, KernelInfo, ModelInfo, RepoType,
1717        RepoTypeDataset, RepoTypeKernel, RepoTypeModel, RepoTypeSpace, SafeTensorsInfo, SpaceInfo, TransformersInfo,
1718        split_repo_id,
1719    };
1720    use crate::client::HFClient;
1721
1722    #[test]
1723    fn test_repo_path_and_accessors() {
1724        let client = HFClient::builder().build().unwrap();
1725        let repo: HFRepository<RepoTypeModel> = HFRepository::new(client, RepoTypeModel, "openai-community", "gpt2");
1726
1727        assert_eq!(repo.owner(), "openai-community");
1728        assert_eq!(repo.name(), "gpt2");
1729        assert_eq!(repo.repo_path(), "openai-community/gpt2");
1730        assert_eq!(repo.repo_type().singular(), "model");
1731    }
1732
1733    #[test]
1734    fn test_marker_struct_singular_and_plural() {
1735        assert_eq!(RepoTypeModel.singular(), "model");
1736        assert_eq!(RepoTypeDataset.singular(), "dataset");
1737        assert_eq!(RepoTypeSpace.singular(), "space");
1738        assert_eq!(RepoTypeKernel.singular(), "kernel");
1739        assert_eq!(RepoTypeModel.plural(), "models");
1740        assert_eq!(RepoTypeDataset.plural(), "datasets");
1741        assert_eq!(RepoTypeSpace.plural(), "spaces");
1742        assert_eq!(RepoTypeKernel.plural(), "kernels");
1743    }
1744
1745    #[test]
1746    fn test_marker_struct_url_prefix() {
1747        assert_eq!(RepoTypeModel.url_prefix(), "");
1748        assert_eq!(RepoTypeDataset.url_prefix(), "datasets/");
1749        assert_eq!(RepoTypeSpace.url_prefix(), "spaces/");
1750        assert_eq!(RepoTypeKernel.url_prefix(), "kernels/");
1751    }
1752
1753    #[test]
1754    fn test_marker_struct_display() {
1755        assert_eq!(format!("{}", RepoTypeModel), "model");
1756        assert_eq!(format!("{}", RepoTypeDataset), "dataset");
1757        assert_eq!(format!("{}", RepoTypeSpace), "space");
1758        assert_eq!(format!("{}", RepoTypeKernel), "kernel");
1759    }
1760
1761    #[test]
1762    fn test_split_repo_id() {
1763        assert_eq!(split_repo_id("user/repo"), (Some("user"), "repo"));
1764        assert_eq!(split_repo_id("repo"), (None, "repo"));
1765        assert_eq!(split_repo_id("org/sub/repo"), (Some("org"), "sub/repo"));
1766    }
1767
1768    #[tokio::test]
1769    async fn test_list_models_limit_zero_returns_empty() {
1770        let client = HFClient::builder().build().unwrap();
1771        let stream = client.list_models().limit(0_usize).send().unwrap();
1772        futures::pin_mut!(stream);
1773        assert!(stream.next().await.is_none());
1774    }
1775
1776    #[tokio::test]
1777    async fn test_list_datasets_limit_zero_returns_empty() {
1778        let client = HFClient::builder().build().unwrap();
1779        let stream = client.list_datasets().limit(0_usize).send().unwrap();
1780        futures::pin_mut!(stream);
1781        assert!(stream.next().await.is_none());
1782    }
1783
1784    #[tokio::test]
1785    async fn test_list_spaces_limit_zero_returns_empty() {
1786        let client = HFClient::builder().build().unwrap();
1787        let stream = client.list_spaces().limit(0_usize).send().unwrap();
1788        futures::pin_mut!(stream);
1789        assert!(stream.next().await.is_none());
1790    }
1791
1792    #[test]
1793    fn test_safetensors_info_deserialize() {
1794        let json = r#"{"parameters":{"F32":124000000,"BF16":1000000},"total":125000000}"#;
1795        let info: SafeTensorsInfo = serde_json::from_str(json).unwrap();
1796        assert_eq!(info.total, 125_000_000);
1797        assert_eq!(info.parameters.get("F32"), Some(&124_000_000));
1798    }
1799
1800    #[test]
1801    fn test_transformers_info_deserialize() {
1802        let json = r#"{"auto_model":"AutoModelForCausalLM","pipeline_tag":"text-generation"}"#;
1803        let info: TransformersInfo = serde_json::from_str(json).unwrap();
1804        assert_eq!(info.auto_model, "AutoModelForCausalLM");
1805        assert_eq!(info.pipeline_tag.as_deref(), Some("text-generation"));
1806        assert!(info.processor.is_none());
1807    }
1808
1809    #[test]
1810    fn test_eval_result_entry_minimal() {
1811        let json = r#"{"dataset":{"id":"cais/hle","task_id":"default"},"value":20.9}"#;
1812        let entry: EvalResultEntry = serde_json::from_str(json).unwrap();
1813        assert_eq!(entry.dataset.id, "cais/hle");
1814        assert_eq!(entry.dataset.task_id, "default");
1815        assert_eq!(entry.value.as_f64(), Some(20.9));
1816        assert!(entry.source.is_none());
1817    }
1818
1819    #[test]
1820    fn test_eval_result_entry_with_source() {
1821        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"}"#;
1822        let entry: EvalResultEntry = serde_json::from_str(json).unwrap();
1823        assert_eq!(entry.dataset.id, "d/x");
1824        assert_eq!(entry.dataset.revision.as_deref(), Some("abc"));
1825        let source = entry.source.as_ref().unwrap();
1826        assert_eq!(source.url.as_deref(), Some("u"));
1827        assert_eq!(source.org.as_deref(), Some("o"));
1828        assert_eq!(entry.verify_token.as_deref(), Some("vt"));
1829    }
1830
1831    #[test]
1832    fn test_inference_provider_mapping_list_form() {
1833        let json = r#"{
1834            "id":"o/m",
1835            "inferenceProviderMapping":[
1836                {"provider":"hf-inference","providerId":"o/m","status":"live","task":"text-generation"}
1837            ]
1838        }"#;
1839        let info: ModelInfo = serde_json::from_str(json).unwrap();
1840        let mappings = info.inference_provider_mapping.unwrap();
1841        assert_eq!(mappings.len(), 1);
1842        assert_eq!(mappings[0].provider, "hf-inference");
1843        assert_eq!(mappings[0].provider_id, "o/m");
1844        assert_eq!(mappings[0].status, "live");
1845    }
1846
1847    #[test]
1848    fn test_inference_provider_mapping_dict_form() {
1849        let json = r#"{
1850            "id":"o/m",
1851            "inferenceProviderMapping":{
1852                "together":{"providerId":"o/m","status":"live","task":"text-generation"}
1853            }
1854        }"#;
1855        let info: ModelInfo = serde_json::from_str(json).unwrap();
1856        let mappings = info.inference_provider_mapping.unwrap();
1857        assert_eq!(mappings.len(), 1);
1858        assert_eq!(mappings[0].provider, "together");
1859        assert_eq!(mappings[0].task, "text-generation");
1860    }
1861
1862    #[test]
1863    fn test_inference_provider_mapping_helper_directly() {
1864        let info_helper: InferenceProviderMapping = serde_json::from_str(
1865            r#"{"provider":"x","providerId":"y","status":"live","task":"t","adapterWeightsPath":"w"}"#,
1866        )
1867        .unwrap();
1868        assert_eq!(info_helper.adapter_weights_path.as_deref(), Some("w"));
1869    }
1870
1871    #[test]
1872    fn test_model_info_ignores_unknown_and_legacy_fields() {
1873        let json = r#"{"id":"o/m","modelId":"o/m","brandNewField":42}"#;
1874        let info: ModelInfo = serde_json::from_str(json).unwrap();
1875        assert_eq!(info.id, "o/m");
1876    }
1877
1878    /// Real `/api/kernels/{repo_id}` response shape (slim — no `tags`,
1879    /// `cardData`, or `siblings`). The `authorData` and `_id` fields are
1880    /// ignored on deserialize but must not break the parse.
1881    #[test]
1882    fn test_kernel_info_deserializes_real_response() {
1883        let json = r#"{
1884            "_id":"69d02879cbdc347de53cced2",
1885            "author":"kernels-community",
1886            "authorData":{"name":"kernels-community","type":"org"},
1887            "trustedPublisher":false,
1888            "downloads":7199,
1889            "gated":false,
1890            "id":"kernels-community/flash-attn2",
1891            "isLikedByUser":false,
1892            "lastModified":"2026-04-20T20:31:57.000Z",
1893            "likes":6,
1894            "private":false,
1895            "repoType":"kernel",
1896            "sha":"e16b327d7c5b015cac48944d4058f688e4d0c62f",
1897            "supportedDriverFamilies":["cuda","xpu","cpu"]
1898        }"#;
1899        let info: KernelInfo = serde_json::from_str(json).unwrap();
1900        assert_eq!(info.id, "kernels-community/flash-attn2");
1901        assert_eq!(info.author.as_deref(), Some("kernels-community"));
1902        assert_eq!(info.sha.as_deref(), Some("e16b327d7c5b015cac48944d4058f688e4d0c62f"));
1903        assert_eq!(info.downloads, Some(7199));
1904        assert_eq!(info.likes, Some(6));
1905        assert_eq!(info.trusted_publisher, Some(false));
1906        assert_eq!(info.supported_driver_families.as_deref(), Some(&["cuda".into(), "xpu".into(), "cpu".into()][..]));
1907        // `gated: false` is kept as JSON (consistent with ModelInfo/SpaceInfo).
1908        assert_eq!(info.gated.as_ref().and_then(|v| v.as_bool()), Some(false));
1909    }
1910
1911    /// `supportedDriverFamilies` is absent on some kernels — must remain optional.
1912    #[test]
1913    fn test_kernel_info_missing_supported_driver_families() {
1914        let json = r#"{"id":"o/k","sha":"abc","downloads":0,"likes":0,"private":false,"gated":false}"#;
1915        let info: KernelInfo = serde_json::from_str(json).unwrap();
1916        assert_eq!(info.id, "o/k");
1917        assert!(info.supported_driver_families.is_none());
1918    }
1919
1920    #[test]
1921    fn test_dataset_info_new_fields() {
1922        let json = r#"{
1923            "id":"u/d",
1924            "citation":"Doe et al. 2024",
1925            "paperswithcode_id":"pwc-id",
1926            "resourceGroup":{"id":"rg-1","name":"Team A"}
1927        }"#;
1928        let info: DatasetInfo = serde_json::from_str(json).unwrap();
1929        assert_eq!(info.citation.as_deref(), Some("Doe et al. 2024"));
1930        assert_eq!(info.paperswithcode_id.as_deref(), Some("pwc-id"));
1931        assert!(info.resource_group.is_some());
1932    }
1933
1934    #[test]
1935    fn test_space_info_new_fields() {
1936        let json = r#"{
1937            "id":"u/s",
1938            "models":["org/model-a","org/model-b"],
1939            "datasets":["org/dataset"],
1940            "resourceGroup":{"id":"rg-2"}
1941        }"#;
1942        let info: SpaceInfo = serde_json::from_str(json).unwrap();
1943        assert_eq!(info.models.as_deref(), Some(&["org/model-a".to_string(), "org/model-b".to_string()][..]));
1944        assert_eq!(info.datasets.as_deref(), Some(&["org/dataset".to_string()][..]));
1945        assert!(info.resource_group.is_some());
1946    }
1947}