1use bon::bon;
12use serde::{Deserialize, Serialize};
13
14use crate::error::HFResult;
15use crate::repository::{HFRepository, RepoTypeSpace, RepoUrl};
16use crate::retry;
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
24#[serde(rename_all = "camelCase")]
25pub struct SpaceRuntime {
26 pub stage: String,
28 #[serde(default)]
31 pub hardware: Option<SpaceHardware>,
32 #[serde(rename = "gcTimeout", default)]
35 pub sleep_time: Option<u64>,
36 #[serde(default)]
38 pub storage: Option<String>,
39 #[serde(default)]
41 pub hot_reloading: Option<SpaceHotReloading>,
42 #[serde(default)]
44 pub volumes: Option<Vec<Volume>>,
45}
46
47#[derive(Debug, Clone, Deserialize, Serialize)]
49pub struct SpaceHardware {
50 #[serde(default)]
53 pub current: Option<String>,
54 #[serde(default)]
57 pub requested: Option<String>,
58}
59
60#[derive(Debug, Clone, Deserialize, Serialize)]
62#[serde(rename_all = "camelCase")]
63pub struct SpaceHotReloading {
64 pub status: String,
66 pub replica_statuses: Vec<serde_json::Value>,
68}
69
70#[derive(Debug, Clone, Deserialize, Serialize)]
72#[serde(rename_all = "camelCase")]
73pub struct Volume {
74 #[serde(rename = "type")]
76 pub r#type: String,
77 pub source: String,
79 pub mount_path: String,
81 #[serde(default)]
83 pub revision: Option<String>,
84 #[serde(default)]
87 pub read_only: Option<bool>,
88 #[serde(default)]
90 pub path: Option<String>,
91}
92
93#[derive(Debug, Clone, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct SpaceVariable {
99 pub key: String,
101 pub value: Option<String>,
103 pub description: Option<String>,
105 pub updated_at: Option<String>,
107}
108
109#[bon]
110impl HFRepository<RepoTypeSpace> {
111 #[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 #[builder(finish_fn = send, derive(Debug, Clone))]
149 pub async fn request_hardware(
150 &self,
151 hardware: &str,
153 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 #[builder(finish_fn = send, derive(Debug, Clone))]
186 pub async fn set_sleep_time(
187 &self,
188 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 #[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 #[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 #[builder(finish_fn = send, derive(Debug, Clone))]
255 pub async fn add_secret(
256 &self,
257 key: &str,
259 value: &str,
261 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 #[builder(finish_fn = send, derive(Debug, Clone))]
293 pub async fn delete_secret(
294 &self,
295 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 #[builder(finish_fn = send, derive(Debug, Clone))]
326 pub async fn add_variable(
327 &self,
328 key: &str,
330 value: &str,
332 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 #[builder(finish_fn = send, derive(Debug, Clone))]
364 pub async fn delete_variable(
365 &self,
366 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 #[builder(finish_fn = send, derive(Debug, Clone))]
432 pub async fn duplicate(
433 &self,
434 to_id: Option<&str>,
437 private: Option<bool>,
439 hardware: Option<&str>,
442 storage: Option<&str>,
445 sleep_time: Option<u64>,
447 secrets: Option<Vec<serde_json::Value>>,
449 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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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 #[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}