pub mod card;
pub mod files;
mod hf;
pub mod record;
pub use hf::HfHub;
use std::{fmt, future::Future, path::PathBuf};
use crate::train::sizing::is_repo_id;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RepoId {
pub namespace: String,
pub name: String,
}
impl RepoId {
#[must_use]
pub fn parse(text: &str) -> Option<Self> {
let (namespace, name) = text.split_once('/')?;
if name.contains('/') || !is_repo_id(text) {
return None;
}
Some(Self {
namespace: namespace.to_string(),
name: name.to_string(),
})
}
}
impl fmt::Display for RepoId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}/{}", self.namespace, self.name)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct UploadFile {
pub local: PathBuf,
pub path_in_repo: String,
pub size: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RepoState {
Created {
private: bool,
},
Existing {
private: bool,
},
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Commit {
pub url: String,
pub oid: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Progress {
pub done: u64,
pub total: u64,
}
pub type ProgressSink = tokio::sync::mpsc::UnboundedSender<Progress>;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum HubError {
#[error("the Hugging Face token is missing or invalid")]
Auth,
#[error("the Hugging Face token needs write access to {namespace}")]
Forbidden {
namespace: String,
},
#[error("Hugging Face rate limit, retry later")]
RateLimited,
#[error("Hugging Face request failed: {0}")]
Other(String),
#[error("cannot build the Hugging Face client: {0}")]
Client(String),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CommitRequest {
pub files: Vec<UploadFile>,
pub card: Option<String>,
pub message: String,
}
pub trait Hub: Send + Sync {
fn whoami(&self) -> impl Future<Output = Result<String, HubError>> + Send;
fn ensure_repo(
&self,
repo: &RepoId,
private: bool,
) -> impl Future<Output = Result<RepoState, HubError>> + Send;
fn remote_card(
&self,
repo: &RepoId,
) -> impl Future<Output = Result<Option<String>, HubError>> + Send;
fn license_of(
&self,
model: &str,
) -> impl Future<Output = Result<Option<String>, HubError>> + Send;
fn upload(
&self,
repo: &RepoId,
commit: CommitRequest,
progress: ProgressSink,
) -> impl Future<Output = Result<Commit, HubError>> + Send;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_accepts_one_slash_and_displays_it_back() {
let repo = RepoId::parse("me/model-1");
assert_eq!(
repo,
Some(RepoId {
namespace: "me".into(),
name: "model-1".into()
})
);
assert_eq!(repo.map(|r| r.to_string()).as_deref(), Some("me/model-1"));
}
#[test]
fn parse_rejects_anything_but_namespace_slash_name() {
for text in ["a", "a/b/c", "/b", "a/", "", "a/b c", ".a/b"] {
assert_eq!(RepoId::parse(text), None, "{text:?} must be rejected");
}
}
}