Skip to main content

hf_hub/
spaces.rs

1//! Space handles, response types, and runtime/hardware/secrets APIs.
2//!
3//! Space-specific operations (runtime, hardware, secrets, variables, pause/restart, duplicate)
4//! live as methods on [`HFRepository<RepoTypeSpace>`](HFRepository). Get a handle with
5//! [`HFClient::space`](crate::HFClient::space) and call the methods directly — there is no separate
6//! `HFSpace` wrapper.
7//!
8//! Response structs such as [`SpaceRuntime`] and [`SpaceVariable`] live in this module; bon-generated
9//! `*Builder` types for each Space API appear here for rustdoc.
10
11use bon::bon;
12use serde::{Deserialize, Serialize};
13
14use crate::error::HFResult;
15use crate::repository::{HFRepository, RepoTypeSpace, RepoUrl};
16use crate::retry;
17
18/// Runtime state of a Space: stage, hardware, storage, and mounted volumes.
19///
20/// Returned by Space lifecycle methods such as
21/// [`runtime`](HFRepository::runtime), [`pause`](HFRepository::pause), and
22/// [`restart`](HFRepository::restart) on [`HFRepository<RepoTypeSpace>`](HFRepository).
23#[derive(Debug, Clone, Deserialize, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct SpaceRuntime {
26    /// Lifecycle stage of the Space (e.g., `"RUNNING"`, `"BUILDING"`, `"PAUSED"`, `"SLEEPING"`).
27    pub stage: String,
28    /// Current and requested hardware for the Space. `None` while a Space is `BUILDING` for the
29    /// first time.
30    #[serde(default)]
31    pub hardware: Option<SpaceHardware>,
32    /// Idle seconds before the Space is put to sleep. `None` means the default policy applies (Spaces
33    /// on free `cpu-basic` hardware sleep after 48 hours; upgraded hardware never sleeps by default).
34    #[serde(rename = "gcTimeout", default)]
35    pub sleep_time: Option<u64>,
36    /// Persistent storage attached to the Space (`"small"`, `"medium"`, or `"large"`), if any.
37    #[serde(default)]
38    pub storage: Option<String>,
39    /// Hot-reloading state for the Space if a hot-reload commit is in progress.
40    #[serde(default)]
41    pub hot_reloading: Option<SpaceHotReloading>,
42    /// Volumes mounted in the Space. `None` if none are attached.
43    #[serde(default)]
44    pub volumes: Option<Vec<Volume>>,
45}
46
47/// Hardware running a Space, with the most recently requested hardware.
48#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct SpaceHardware {
50    /// Hardware currently running the Space (e.g., `"cpu-basic"`, `"t4-medium"`). `None` if no
51    /// hardware is assigned yet.
52    #[serde(default)]
53    pub current: Option<String>,
54    /// Hardware most recently requested for the Space. May differ from `current` if a request is
55    /// in flight. `None` if no hardware has been requested yet.
56    #[serde(default)]
57    pub requested: Option<String>,
58}
59
60/// Hot-reloading state for a Space.
61#[derive(Debug, Clone, Deserialize, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct SpaceHotReloading {
64    /// Status of the hot-reload commit, e.g., `"created"` or `"canceled"`.
65    pub status: String,
66    /// Per-replica statuses, each a `[replica_hash, status]` pair.
67    pub replica_statuses: Vec<serde_json::Value>,
68}
69
70/// A volume mounted inside a Space (or Job) container.
71#[derive(Debug, Clone, Deserialize, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct Volume {
74    /// Volume kind: `"bucket"`, `"model"`, `"dataset"`, or `"space"`.
75    #[serde(rename = "type")]
76    pub r#type: String,
77    /// Source identifier, e.g., `"username/my-bucket"` or `"username/my-model"`.
78    pub source: String,
79    /// Mount path inside the container (must start with `/`).
80    pub mount_path: String,
81    /// Git revision for repo-backed volumes; defaults to `"main"` server-side when omitted.
82    #[serde(default)]
83    pub revision: Option<String>,
84    /// Whether the mount is read-only. Forced to `true` for repo-backed volumes; defaults to `false`
85    /// for buckets.
86    #[serde(default)]
87    pub read_only: Option<bool>,
88    /// Subfolder inside the source to mount (e.g., `"path/to/dir"`).
89    #[serde(default)]
90    pub path: Option<String>,
91}
92
93/// A public environment variable set on a Space (non-secret).
94///
95/// Secrets are not returned — only variables declared via the Space's variables API.
96#[derive(Debug, Clone, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct SpaceVariable {
99    /// Variable name (e.g., `"MODEL_REPO_ID"`).
100    pub key: String,
101    /// Variable value. `None` if the Hub returns the variable without a value.
102    pub value: Option<String>,
103    /// Human-readable description of what the variable is for.
104    pub description: Option<String>,
105    /// ISO-8601 timestamp of the last update to this variable, if it has been updated since creation.
106    pub updated_at: Option<String>,
107}
108
109#[bon]
110impl HFRepository<RepoTypeSpace> {
111    /// Fetch the current runtime state of the Space (hardware, stage, URL, etc.).
112    ///
113    /// Endpoint: `GET /api/spaces/{repo_id}/runtime`.
114    ///
115    /// # Example
116    ///
117    /// ```rust,no_run
118    /// # use hf_hub::HFClient;
119    /// # #[tokio::main] async fn main() -> hf_hub::HFResult<()> {
120    /// let client = HFClient::builder().build()?;
121    /// let runtime = client.space("owner", "name").runtime().send().await?;
122    /// # let _ = runtime;
123    /// # Ok(()) }
124    /// ```
125    #[builder(finish_fn = send, derive(Debug, Clone))]
126    pub async fn runtime(&self) -> HFResult<SpaceRuntime> {
127        let url = format!("{}/api/spaces/{}/runtime", self.hf_client.endpoint(), self.repo_path());
128        let headers = self.hf_client.auth_headers();
129        let response = retry::retry(self.hf_client.retry_config(), || {
130            self.hf_client.http_client().get(&url).headers(headers.clone()).send()
131        })
132        .await?;
133        let response = self
134            .hf_client
135            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
136            .await?;
137        Ok(response.json().await?)
138    }
139
140    /// Request an upgrade or downgrade of the Space's hardware tier.
141    ///
142    /// Endpoint: `POST /api/spaces/{repo_id}/hardware`.
143    ///
144    /// # Parameters
145    ///
146    /// - `hardware` (required): hardware flavor to request (e.g., `"cpu-basic"`, `"t4-small"`, `"a10g-small"`).
147    /// - `sleep_time`: seconds of inactivity before the Space is put to sleep. `0` means never sleep.
148    #[builder(finish_fn = send, derive(Debug, Clone))]
149    pub async fn request_hardware(
150        &self,
151        /// Hardware flavor to request (e.g., `"cpu-basic"`, `"t4-small"`, `"a10g-small"`).
152        hardware: &str,
153        /// Seconds of inactivity before the Space is put to sleep. `0` means never sleep.
154        sleep_time: Option<u64>,
155    ) -> HFResult<SpaceRuntime> {
156        let url = format!("{}/api/spaces/{}/hardware", self.hf_client.endpoint(), self.repo_path());
157        let mut body = serde_json::json!({ "flavor": hardware });
158        if let Some(sleep_time) = sleep_time {
159            body["sleepTime"] = serde_json::json!(sleep_time);
160        }
161        let headers = self.hf_client.auth_headers();
162        let response = retry::retry(self.hf_client.retry_config(), || {
163            self.hf_client
164                .http_client()
165                .post(&url)
166                .headers(headers.clone())
167                .json(&body)
168                .send()
169        })
170        .await?;
171        let response = self
172            .hf_client
173            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
174            .await?;
175        Ok(response.json().await?)
176    }
177
178    /// Configure the number of seconds of inactivity before the Space is put to sleep.
179    ///
180    /// Endpoint: `POST /api/spaces/{repo_id}/sleeptime`.
181    ///
182    /// # Parameters
183    ///
184    /// - `sleep_time` (required): seconds of inactivity before the Space is put to sleep. `0` means never sleep.
185    #[builder(finish_fn = send, derive(Debug, Clone))]
186    pub async fn set_sleep_time(
187        &self,
188        /// Seconds of inactivity before the Space is put to sleep. `0` means never sleep.
189        sleep_time: u64,
190    ) -> HFResult<()> {
191        let url = format!("{}/api/spaces/{}/sleeptime", self.hf_client.endpoint(), self.repo_path());
192        let body = serde_json::json!({ "seconds": sleep_time });
193        let headers = self.hf_client.auth_headers();
194        let response = retry::retry(self.hf_client.retry_config(), || {
195            self.hf_client
196                .http_client()
197                .post(&url)
198                .headers(headers.clone())
199                .json(&body)
200                .send()
201        })
202        .await?;
203        self.hf_client
204            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
205            .await?;
206        Ok(())
207    }
208
209    /// Pause the Space, stopping it from consuming compute resources.
210    ///
211    /// Endpoint: `POST /api/spaces/{repo_id}/pause`.
212    #[builder(finish_fn = send, derive(Debug, Clone))]
213    pub async fn pause(&self) -> HFResult<SpaceRuntime> {
214        let url = format!("{}/api/spaces/{}/pause", self.hf_client.endpoint(), self.repo_path());
215        let headers = self.hf_client.auth_headers();
216        let response = retry::retry(self.hf_client.retry_config(), || {
217            self.hf_client.http_client().post(&url).headers(headers.clone()).send()
218        })
219        .await?;
220        let response = self
221            .hf_client
222            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
223            .await?;
224        Ok(response.json().await?)
225    }
226
227    /// Restart a paused or errored Space.
228    ///
229    /// Endpoint: `POST /api/spaces/{repo_id}/restart`.
230    #[builder(finish_fn = send, derive(Debug, Clone))]
231    pub async fn restart(&self) -> HFResult<SpaceRuntime> {
232        let url = format!("{}/api/spaces/{}/restart", self.hf_client.endpoint(), self.repo_path());
233        let headers = self.hf_client.auth_headers();
234        let response = retry::retry(self.hf_client.retry_config(), || {
235            self.hf_client.http_client().post(&url).headers(headers.clone()).send()
236        })
237        .await?;
238        let response = self
239            .hf_client
240            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
241            .await?;
242        Ok(response.json().await?)
243    }
244
245    /// Add or update a secret (encrypted environment variable) on the Space.
246    ///
247    /// Endpoint: `POST /api/spaces/{repo_id}/secrets`.
248    ///
249    /// # Parameters
250    ///
251    /// - `key` (required): secret key name.
252    /// - `value` (required): secret value.
253    /// - `description`: human-readable description of the secret.
254    #[builder(finish_fn = send, derive(Debug, Clone))]
255    pub async fn add_secret(
256        &self,
257        /// Secret key name.
258        key: &str,
259        /// Secret value.
260        value: &str,
261        /// Human-readable description of the secret.
262        description: Option<&str>,
263    ) -> HFResult<()> {
264        let url = format!("{}/api/spaces/{}/secrets", self.hf_client.endpoint(), self.repo_path());
265        let mut body = serde_json::json!({ "key": key, "value": value });
266        if let Some(ref desc) = description {
267            body["description"] = serde_json::json!(desc);
268        }
269        let headers = self.hf_client.auth_headers();
270        let response = retry::retry(self.hf_client.retry_config(), || {
271            self.hf_client
272                .http_client()
273                .post(&url)
274                .headers(headers.clone())
275                .json(&body)
276                .send()
277        })
278        .await?;
279        self.hf_client
280            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
281            .await?;
282        Ok(())
283    }
284
285    /// Delete a secret from the Space by key.
286    ///
287    /// Endpoint: `DELETE /api/spaces/{repo_id}/secrets`.
288    ///
289    /// # Parameters
290    ///
291    /// - `key` (required): secret key name to delete.
292    #[builder(finish_fn = send, derive(Debug, Clone))]
293    pub async fn delete_secret(
294        &self,
295        /// Secret key name to delete.
296        key: &str,
297    ) -> HFResult<()> {
298        let url = format!("{}/api/spaces/{}/secrets", self.hf_client.endpoint(), self.repo_path());
299        let body = serde_json::json!({ "key": key });
300        let headers = self.hf_client.auth_headers();
301        let response = retry::retry(self.hf_client.retry_config(), || {
302            self.hf_client
303                .http_client()
304                .delete(&url)
305                .headers(headers.clone())
306                .json(&body)
307                .send()
308        })
309        .await?;
310        self.hf_client
311            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
312            .await?;
313        Ok(())
314    }
315
316    /// Add or update a public environment variable on the Space.
317    ///
318    /// Endpoint: `POST /api/spaces/{repo_id}/variables`.
319    ///
320    /// # Parameters
321    ///
322    /// - `key` (required): variable key name.
323    /// - `value` (required): variable value.
324    /// - `description`: human-readable description of the variable.
325    #[builder(finish_fn = send, derive(Debug, Clone))]
326    pub async fn add_variable(
327        &self,
328        /// Variable key name.
329        key: &str,
330        /// Variable value.
331        value: &str,
332        /// Human-readable description of the variable.
333        description: Option<&str>,
334    ) -> HFResult<()> {
335        let url = format!("{}/api/spaces/{}/variables", self.hf_client.endpoint(), self.repo_path());
336        let mut body = serde_json::json!({ "key": key, "value": value });
337        if let Some(ref desc) = description {
338            body["description"] = serde_json::json!(desc);
339        }
340        let headers = self.hf_client.auth_headers();
341        let response = retry::retry(self.hf_client.retry_config(), || {
342            self.hf_client
343                .http_client()
344                .post(&url)
345                .headers(headers.clone())
346                .json(&body)
347                .send()
348        })
349        .await?;
350        self.hf_client
351            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
352            .await?;
353        Ok(())
354    }
355
356    /// Delete a public environment variable from the Space by key.
357    ///
358    /// Endpoint: `DELETE /api/spaces/{repo_id}/variables`.
359    ///
360    /// # Parameters
361    ///
362    /// - `key` (required): variable key name to delete.
363    #[builder(finish_fn = send, derive(Debug, Clone))]
364    pub async fn delete_variable(
365        &self,
366        /// Variable key name to delete.
367        key: &str,
368    ) -> HFResult<()> {
369        let url = format!("{}/api/spaces/{}/variables", self.hf_client.endpoint(), self.repo_path());
370        let body = serde_json::json!({ "key": key });
371        let headers = self.hf_client.auth_headers();
372        let response = retry::retry(self.hf_client.retry_config(), || {
373            self.hf_client
374                .http_client()
375                .delete(&url)
376                .headers(headers.clone())
377                .json(&body)
378                .send()
379        })
380        .await?;
381        self.hf_client
382            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
383            .await?;
384        Ok(())
385    }
386
387    /// Duplicate this Space to a new repository.
388    ///
389    /// Endpoint: `POST /api/spaces/{repo_id}/duplicate`.
390    ///
391    /// # Parameters
392    ///
393    /// - `to_id`: destination repository ID in `"owner/name"` format. Defaults to the authenticated user's namespace
394    ///   with the same name.
395    /// - `private`: whether the duplicated Space should be private.
396    /// - `hardware`: hardware flavor identifier the duplicated Space should run on (see `Hardware values` below). When
397    ///   omitted, the Hub picks a default (typically `"cpu-basic"`) — pass a value explicitly to mirror the source
398    ///   Space's hardware.
399    /// - `storage`: persistent storage tier identifier. One of `"small"`, `"medium"`, or `"large"`. Omit to duplicate
400    ///   without persistent storage. See the [Spaces storage docs](https://huggingface.co/docs/hub/spaces-storage) for
401    ///   current sizes.
402    /// - `sleep_time`: seconds of inactivity before the Space is put to sleep. `0` means never sleep.
403    /// - `secrets`: encrypted environment variables to set on the duplicated Space (see `Secret/variable shape` below).
404    /// - `variables`: public (non-secret) environment variables to set on the duplicated Space (see `Secret/variable
405    ///   shape` below).
406    ///
407    /// # Hardware values
408    ///
409    /// The Hub uses lowercase, hyphenated identifiers. Common values include `"cpu-basic"`, `"cpu-upgrade"`,
410    /// `"t4-small"`, `"t4-medium"`, `"l4x1"`, `"l4x4"`, `"a10g-small"`, `"a10g-large"`, `"a10g-largex2"`,
411    /// `"a10g-largex4"`, `"a100-large"`, `"h100"`, `"h100x8"`, and `"zero-a10g"`. The authoritative, current list lives
412    /// in the [Spaces GPU hardware docs](https://huggingface.co/docs/hub/spaces-gpus).
413    ///
414    /// # Secret/variable shape
415    ///
416    /// Each entry in `secrets` or `variables` is a JSON object with the following keys:
417    ///
418    /// - `key` (string, required): variable/secret name.
419    /// - `value` (string, required): variable/secret value.
420    /// - `description` (string, optional): human-readable description.
421    ///
422    /// ```rust,no_run
423    /// use serde_json::json;
424    ///
425    /// let secrets = vec![
426    ///     json!({ "key": "HF_TOKEN", "value": "hf_...", "description": "API token" }),
427    ///     json!({ "key": "DB_PASSWORD", "value": "s3cret" }),
428    /// ];
429    /// let variables = vec![json!({ "key": "MODEL_NAME", "value": "bert-base-uncased" })];
430    /// ```
431    #[builder(finish_fn = send, derive(Debug, Clone))]
432    pub async fn duplicate(
433        &self,
434        /// Destination repository ID in `"owner/name"` format. Defaults to the authenticated user's namespace
435        /// with the same name.
436        to_id: Option<&str>,
437        /// Whether the duplicated Space should be private.
438        private: Option<bool>,
439        /// Hardware flavor identifier the duplicated Space should run on. When omitted, the Hub picks a default
440        /// (typically `"cpu-basic"`) — pass a value explicitly to mirror the source Space's hardware.
441        hardware: Option<&str>,
442        /// Persistent storage tier identifier. One of `"small"`, `"medium"`, or `"large"`. Omit to duplicate
443        /// without persistent storage.
444        storage: Option<&str>,
445        /// Seconds of inactivity before the Space is put to sleep. `0` means never sleep.
446        sleep_time: Option<u64>,
447        /// Encrypted environment variables to set on the duplicated Space.
448        secrets: Option<Vec<serde_json::Value>>,
449        /// Public (non-secret) environment variables to set on the duplicated Space.
450        variables: Option<Vec<serde_json::Value>>,
451    ) -> HFResult<RepoUrl> {
452        let url = format!("{}/api/spaces/{}/duplicate", self.hf_client.endpoint(), self.repo_path());
453        let mut body = serde_json::Map::new();
454        if let Some(ref to_id) = to_id {
455            body.insert("repository".into(), serde_json::json!(to_id));
456        }
457        if let Some(private) = private {
458            body.insert("private".into(), serde_json::json!(private));
459        }
460        if let Some(ref hw) = hardware {
461            body.insert("hardware".into(), serde_json::json!(hw));
462        }
463        if let Some(ref storage) = storage {
464            body.insert("storage".into(), serde_json::json!(storage));
465        }
466        if let Some(sleep_time) = sleep_time {
467            body.insert("sleepTime".into(), serde_json::json!(sleep_time));
468        }
469        if let Some(ref secrets) = secrets {
470            body.insert("secrets".into(), serde_json::json!(secrets));
471        }
472        if let Some(ref variables) = variables {
473            body.insert("variables".into(), serde_json::json!(variables));
474        }
475        let headers = self.hf_client.auth_headers();
476        let response = retry::retry(self.hf_client.retry_config(), || {
477            self.hf_client
478                .http_client()
479                .post(&url)
480                .headers(headers.clone())
481                .json(&body)
482                .send()
483        })
484        .await?;
485        let response = self
486            .hf_client
487            .check_response(response, Some(&self.repo_path()), crate::error::NotFoundContext::Repo)
488            .await?;
489        Ok(response.json().await?)
490    }
491}
492
493#[cfg(feature = "blocking")]
494#[bon]
495impl crate::blocking::HFRepositorySync<RepoTypeSpace> {
496    /// Blocking counterpart of [`HFRepository::runtime`]. See the async method for parameters and
497    /// behavior.
498    #[builder(finish_fn = send, derive(Debug, Clone))]
499    pub fn runtime(&self) -> HFResult<SpaceRuntime> {
500        self.runtime.block_on(self.inner.runtime().send())
501    }
502
503    /// Blocking counterpart of [`HFRepository::request_hardware`]. See the async method for parameters
504    /// and behavior.
505    #[builder(finish_fn = send, derive(Debug, Clone))]
506    pub fn request_hardware(&self, hardware: &str, sleep_time: Option<u64>) -> HFResult<SpaceRuntime> {
507        self.runtime.block_on(
508            self.inner
509                .request_hardware()
510                .hardware(hardware)
511                .maybe_sleep_time(sleep_time)
512                .send(),
513        )
514    }
515
516    /// Blocking counterpart of [`HFRepository::set_sleep_time`]. See the async method for parameters
517    /// and behavior.
518    #[builder(finish_fn = send, derive(Debug, Clone))]
519    pub fn set_sleep_time(&self, sleep_time: u64) -> HFResult<()> {
520        self.runtime.block_on(self.inner.set_sleep_time().sleep_time(sleep_time).send())
521    }
522
523    /// Blocking counterpart of [`HFRepository::pause`]. See the async method for parameters and
524    /// behavior.
525    #[builder(finish_fn = send, derive(Debug, Clone))]
526    pub fn pause(&self) -> HFResult<SpaceRuntime> {
527        self.runtime.block_on(self.inner.pause().send())
528    }
529
530    /// Blocking counterpart of [`HFRepository::restart`]. See the async method for parameters and
531    /// behavior.
532    #[builder(finish_fn = send, derive(Debug, Clone))]
533    pub fn restart(&self) -> HFResult<SpaceRuntime> {
534        self.runtime.block_on(self.inner.restart().send())
535    }
536
537    /// Blocking counterpart of [`HFRepository::add_secret`]. See the async method for parameters and
538    /// behavior.
539    #[builder(finish_fn = send, derive(Debug, Clone))]
540    pub fn add_secret(&self, key: &str, value: &str, description: Option<&str>) -> HFResult<()> {
541        self.runtime.block_on(
542            self.inner
543                .add_secret()
544                .key(key)
545                .value(value)
546                .maybe_description(description)
547                .send(),
548        )
549    }
550
551    /// Blocking counterpart of [`HFRepository::delete_secret`]. See the async method for parameters and
552    /// behavior.
553    #[builder(finish_fn = send, derive(Debug, Clone))]
554    pub fn delete_secret(&self, key: &str) -> HFResult<()> {
555        self.runtime.block_on(self.inner.delete_secret().key(key).send())
556    }
557
558    /// Blocking counterpart of [`HFRepository::add_variable`]. See the async method for parameters and
559    /// behavior.
560    #[builder(finish_fn = send, derive(Debug, Clone))]
561    pub fn add_variable(&self, key: &str, value: &str, description: Option<&str>) -> HFResult<()> {
562        self.runtime.block_on(
563            self.inner
564                .add_variable()
565                .key(key)
566                .value(value)
567                .maybe_description(description)
568                .send(),
569        )
570    }
571
572    /// Blocking counterpart of [`HFRepository::delete_variable`]. See the async method for parameters
573    /// and behavior.
574    #[builder(finish_fn = send, derive(Debug, Clone))]
575    pub fn delete_variable(&self, key: &str) -> HFResult<()> {
576        self.runtime.block_on(self.inner.delete_variable().key(key).send())
577    }
578
579    /// Blocking counterpart of [`HFRepository::duplicate`]. See the async method for parameters and
580    /// behavior.
581    #[builder(finish_fn = send, derive(Debug, Clone))]
582    pub fn duplicate(
583        &self,
584        to_id: Option<&str>,
585        private: Option<bool>,
586        hardware: Option<&str>,
587        storage: Option<&str>,
588        sleep_time: Option<u64>,
589        secrets: Option<Vec<serde_json::Value>>,
590        variables: Option<Vec<serde_json::Value>>,
591    ) -> HFResult<RepoUrl> {
592        self.runtime.block_on(
593            self.inner
594                .duplicate()
595                .maybe_to_id(to_id)
596                .maybe_private(private)
597                .maybe_hardware(hardware)
598                .maybe_storage(storage)
599                .maybe_sleep_time(sleep_time)
600                .maybe_secrets(secrets)
601                .maybe_variables(variables)
602                .send(),
603        )
604    }
605}
606
607#[cfg(test)]
608mod tests {
609    use super::{SpaceRuntime, SpaceVariable};
610    use crate::repository::RepoType;
611
612    #[test]
613    fn test_space_handle_constructor() {
614        let client = crate::HFClient::builder().build().unwrap();
615        let space = client.space("huggingface-projects", "diffusers-gallery");
616
617        assert_eq!(space.repo_type().singular(), "space");
618        assert_eq!(space.repo_path(), "huggingface-projects/diffusers-gallery");
619    }
620
621    #[test]
622    fn test_space_runtime_deserialize_hardware() {
623        let json = r#"{"stage":"RUNNING","hardware":{"current":"cpu-basic","requested":"t4-medium"},"storage":null}"#;
624        let runtime: SpaceRuntime = serde_json::from_str(json).unwrap();
625        assert_eq!(runtime.stage, "RUNNING");
626        let hardware = runtime.hardware.as_ref().unwrap();
627        assert_eq!(hardware.current.as_deref(), Some("cpu-basic"));
628        assert_eq!(hardware.requested.as_deref(), Some("t4-medium"));
629        assert_eq!(runtime.storage, None);
630    }
631
632    #[test]
633    fn test_space_runtime_deserialize_minimal() {
634        let json = r#"{"stage":"BUILDING"}"#;
635        let runtime: SpaceRuntime = serde_json::from_str(json).unwrap();
636        assert_eq!(runtime.stage, "BUILDING");
637        assert!(runtime.hardware.is_none());
638    }
639
640    #[test]
641    fn test_space_runtime_sleep_time_from_gc_timeout() {
642        let json = r#"{"stage":"RUNNING","gcTimeout":172800}"#;
643        let runtime: SpaceRuntime = serde_json::from_str(json).unwrap();
644        assert_eq!(runtime.sleep_time, Some(172800));
645    }
646
647    #[test]
648    fn test_space_runtime_volumes_and_hot_reloading() {
649        let json = r#"{
650            "stage":"RUNNING",
651            "volumes":[{"type":"model","source":"u/m","mountPath":"/data","readOnly":true}],
652            "hotReloading":{"status":"created","replicaStatuses":[]}
653        }"#;
654        let runtime: SpaceRuntime = serde_json::from_str(json).unwrap();
655        let volumes = runtime.volumes.as_ref().unwrap();
656        assert_eq!(volumes.len(), 1);
657        assert_eq!(volumes[0].r#type, "model");
658        assert_eq!(volumes[0].mount_path, "/data");
659        assert_eq!(volumes[0].read_only, Some(true));
660        assert_eq!(runtime.hot_reloading.as_ref().unwrap().status, "created");
661    }
662
663    #[test]
664    fn test_space_runtime_ignores_unknown_fields() {
665        let json = r#"{"stage":"RUNNING","replicas":{"current":1},"someNewField":42}"#;
666        let runtime: SpaceRuntime = serde_json::from_str(json).unwrap();
667        assert_eq!(runtime.stage, "RUNNING");
668    }
669
670    #[test]
671    fn test_space_variable_deserialize() {
672        let json = r#"{"key":"MODEL_ID","value":"gpt2","description":"The model"}"#;
673        let var: SpaceVariable = serde_json::from_str(json).unwrap();
674        assert_eq!(var.key, "MODEL_ID");
675        assert_eq!(var.value.as_deref(), Some("gpt2"));
676    }
677}