1pub mod changes;
2pub mod commits;
3pub mod data_frames;
4pub mod files;
5
6use std::path::Path;
7
8pub use commits::commit;
9
10use crate::api;
11use crate::api::client;
12use crate::error::OxenError;
13use crate::model::RemoteRepository;
14use crate::view::workspaces::{ListWorkspaceResponseView, WorkspaceResponseWithStatus};
15use crate::view::workspaces::{NewWorkspace, WorkspaceResponse};
16use crate::view::{StatusMessage, WorkspaceResponseView};
17
18pub async fn list(remote_repo: &RemoteRepository) -> Result<Vec<WorkspaceResponse>, OxenError> {
19 let url = api::endpoint::url_from_repo(remote_repo, "/workspaces")?;
20 let client = client::new_for_url(&url)?;
21 let res = client.get(&url).send().await?;
22 let body = client::parse_json_body(&url, res).await?;
23 let response: Result<ListWorkspaceResponseView, serde_json::Error> =
24 serde_json::from_str(&body);
25 match response {
26 Ok(val) => Ok(val.workspaces),
27 Err(err) => Err(OxenError::basic_str(format!(
28 "error parsing response from {url}\n\nErr {err:?} \n\n{body}"
29 ))),
30 }
31}
32
33pub async fn get(
34 remote_repo: &RemoteRepository,
35 workspace_id: impl AsRef<str>,
36) -> Result<Option<WorkspaceResponse>, OxenError> {
37 let workspace_id = workspace_id.as_ref();
38 let url = api::endpoint::url_from_repo(remote_repo, &format!("/workspaces/{workspace_id}"))?;
39 let client = client::new_for_url(&url)?;
40 let res = client.get(&url).send().await?;
41 let body_result = client::parse_json_body(&url, res).await;
42
43 let workspace = body_result
44 .ok()
45 .and_then(|body| serde_json::from_str::<WorkspaceResponseView>(&body).ok())
46 .map(|val| val.workspace);
47
48 Ok(workspace)
49}
50
51pub async fn get_by_name(
52 remote_repo: &RemoteRepository,
53 name: impl AsRef<str>,
54) -> Result<Option<WorkspaceResponse>, OxenError> {
55 let name = name.as_ref();
56 let url = api::endpoint::url_from_repo(remote_repo, &format!("/workspaces?name={name}"))?;
57 let client = client::new_for_url(&url)?;
58 let res = client.get(&url).send().await?;
59 let body = client::parse_json_body(&url, res).await?;
60 let response: Result<ListWorkspaceResponseView, serde_json::Error> =
61 serde_json::from_str(&body);
62 match response {
63 Ok(val) => {
64 if val.workspaces.len() == 1 {
65 Ok(Some(val.workspaces[0].clone()))
66 } else if val.workspaces.is_empty() {
67 Ok(None)
68 } else {
69 Err(OxenError::basic_str(format!(
70 "expected 1 workspace, got {}",
71 val.workspaces.len()
72 )))
73 }
74 }
75 Err(err) => Err(OxenError::basic_str(format!(
76 "error parsing response from {url}\n\nErr {err:?} \n\n{body}"
77 ))),
78 }
79}
80
81pub async fn create(
82 remote_repo: &RemoteRepository,
83 branch_name: impl AsRef<str>,
84 workspace_id: impl AsRef<str>,
85) -> Result<WorkspaceResponseWithStatus, OxenError> {
86 create_with_path(remote_repo, branch_name, workspace_id, Path::new("/"), None).await
87}
88
89pub async fn create_with_name(
90 remote_repo: &RemoteRepository,
91 branch_name: impl AsRef<str>,
92 workspace_id: impl AsRef<str>,
93 workspace_name: impl AsRef<str>,
94) -> Result<WorkspaceResponseWithStatus, OxenError> {
95 let workspace_name = workspace_name.as_ref().to_string();
96 create_with_path(
97 remote_repo,
98 branch_name,
99 workspace_id,
100 Path::new("/"),
101 Some(workspace_name),
102 )
103 .await
104}
105
106pub async fn create_with_path(
107 remote_repo: &RemoteRepository,
108 branch_name: impl AsRef<str>,
109 workspace_id: impl AsRef<str>,
110 path: impl AsRef<Path>,
111 workspace_name: Option<String>,
112) -> Result<WorkspaceResponseWithStatus, OxenError> {
113 let branch_name = branch_name.as_ref();
114 let workspace_id = workspace_id.as_ref();
115 let path = path.as_ref();
116 let url = api::endpoint::url_from_repo(remote_repo, "/workspaces")?;
117 log::debug!("create workspace {url}\n");
118
119 let body = NewWorkspace {
120 branch_name: branch_name.to_string(),
121 workspace_id: workspace_id.to_string(),
122 resource_path: Some(path.to_str().unwrap().to_string()),
124 entity_type: Some("user".to_string()),
125 name: workspace_name,
126 force: Some(true),
127 };
128
129 let client = client::new_for_url(&url)?;
130 let res = client.put(&url).json(&body).send().await?;
131
132 let body = client::parse_json_body(&url, res).await?;
133 log::debug!("create workspace got body: {body}");
134 let response: Result<WorkspaceResponseView, serde_json::Error> = serde_json::from_str(&body);
135 match response {
136 Ok(val) => Ok(WorkspaceResponseWithStatus {
137 id: val.workspace.id,
138 name: val.workspace.name,
139 commit: val.workspace.commit,
140 status: val.status.status_message,
141 }),
142 Err(err) => Err(OxenError::basic_str(format!(
143 "error parsing response from {url}\n\nErr {err:?} \n\n{body}"
144 ))),
145 }
146}
147
148pub async fn delete(
149 remote_repo: &RemoteRepository,
150 workspace_id: impl AsRef<str>,
151) -> Result<WorkspaceResponse, OxenError> {
152 let workspace_id = workspace_id.as_ref();
153 let url = api::endpoint::url_from_repo(remote_repo, &format!("/workspaces/{workspace_id}"))?;
154 log::debug!("delete workspace {url}\n");
155
156 let client = client::new_for_url(&url)?;
157 let res = client.delete(&url).send().await?;
158
159 let body = client::parse_json_body(&url, res).await?;
160 log::debug!("delete workspace got body: {body}");
161 let response: Result<WorkspaceResponseView, serde_json::Error> = serde_json::from_str(&body);
162 match response {
163 Ok(val) => Ok(val.workspace),
164 Err(err) => Err(OxenError::basic_str(format!(
165 "error parsing response from {url}\n\nErr {err:?} \n\n{body}"
166 ))),
167 }
168}
169
170pub async fn clear(remote_repo: &RemoteRepository) -> Result<(), OxenError> {
171 let url = api::endpoint::url_from_repo(remote_repo, "/workspaces")?;
172 log::debug!("clear workspaces {url}\n");
173
174 let client = client::new_for_url(&url)?;
175 let res = client.delete(&url).send().await?;
176
177 let body = client::parse_json_body(&url, res).await?;
178 log::debug!("delete workspace got body: {body}");
179 let response: Result<StatusMessage, serde_json::Error> = serde_json::from_str(&body);
180 match response {
181 Ok(_) => Ok(()),
182 Err(err) => Err(OxenError::basic_str(format!(
183 "error parsing response from {url}\n\nErr {err:?} \n\n{body}"
184 ))),
185 }
186}
187
188#[cfg(test)]
189mod tests {
190
191 use super::*;
192
193 use crate::api;
194 use crate::command;
195 use crate::constants;
196 use crate::constants::DEFAULT_BRANCH_NAME;
197 use crate::error::OxenError;
198 use crate::model::NewCommitBody;
199 use crate::opts::DFOpts;
200 use crate::repositories;
201 use crate::test;
202
203 #[tokio::test]
204 async fn test_create_workspace() -> Result<(), OxenError> {
205 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
206 let branch_name = "main";
207 let workspace_id = "test_workspace_id";
208 let workspace = create(&remote_repo, branch_name, workspace_id).await?;
209
210 assert_eq!(workspace.id, workspace_id);
211
212 Ok(remote_repo)
213 })
214 .await
215 }
216
217 #[tokio::test]
218 async fn test_create_workspace_with_name() -> Result<(), OxenError> {
219 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
220 let branch_name = "main";
221 let workspace_id = "test_workspace_id";
222 let workspace_name = "test_workspace_name";
223 let workspace =
224 create_with_name(&remote_repo, branch_name, workspace_id, workspace_name).await?;
225
226 assert_eq!(workspace.id, workspace_id);
227 assert_eq!(workspace.name, Some(workspace_name.to_string()));
228
229 let workspace = get(&remote_repo, &workspace_id).await?;
230 assert!(workspace.is_some());
231 assert_eq!(
232 workspace.as_ref().unwrap().name,
233 Some(workspace_name.to_string())
234 );
235 assert_eq!(workspace.as_ref().unwrap().id, workspace_id);
236
237 let workspace = get_by_name(&remote_repo, &workspace_name).await?;
238 assert!(workspace.is_some());
239 assert_eq!(
240 workspace.as_ref().unwrap().name,
241 Some(workspace_name.to_string())
242 );
243 assert_eq!(workspace.as_ref().unwrap().id, workspace_id);
244
245 Ok(remote_repo)
246 })
247 .await
248 }
249
250 #[tokio::test]
251 async fn test_get_workspace_by_name() -> Result<(), OxenError> {
252 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
253 let branch_name = "main";
254 let workspace_id = "test_workspace_id";
255 let workspace_name = "test_workspace_name";
256 create_with_name(&remote_repo, branch_name, workspace_id, workspace_name).await?;
257
258 let workspace_id2 = "test_workspace_id2";
260 let workspace_name2 = "test_workspace_name2";
261 create_with_name(&remote_repo, branch_name, workspace_id2, workspace_name2).await?;
262
263 let workspace = get_by_name(&remote_repo, &workspace_name).await?;
264 assert!(workspace.is_some());
265 assert_eq!(
266 workspace.as_ref().unwrap().name,
267 Some(workspace_name.to_string())
268 );
269 assert_eq!(workspace.as_ref().unwrap().id, workspace_id);
270
271 let workspace2 = get_by_name(&remote_repo, &workspace_name2).await?;
272 assert!(workspace2.is_some());
273 assert_eq!(
274 workspace2.as_ref().unwrap().name,
275 Some(workspace_name2.to_string())
276 );
277 assert_eq!(workspace2.as_ref().unwrap().id, workspace_id2);
278
279 Ok(remote_repo)
280 })
281 .await
282 }
283
284 #[tokio::test]
285 async fn test_get_workspace_by_name_does_not_exist() -> Result<(), OxenError> {
286 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
287 let workspace_name = "name_does_not_exist";
288
289 let workspace = get_by_name(&remote_repo, &workspace_name).await?;
290 assert!(workspace.is_none());
291
292 Ok(remote_repo)
293 })
294 .await
295 }
296
297 #[tokio::test]
298 async fn test_get_workspace_by_id_does_not_exist() -> Result<(), OxenError> {
299 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
300 let workspace_id = "id_does_not_exist";
301
302 let workspace = get(&remote_repo, &workspace_id).await?;
303 assert!(workspace.is_none());
304
305 Ok(remote_repo)
306 })
307 .await
308 }
309
310 #[tokio::test]
311 async fn test_clear_workspaces() -> Result<(), OxenError> {
312 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
313 for i in 0..10 {
315 create(
316 &remote_repo,
317 DEFAULT_BRANCH_NAME,
318 &format!("test_workspace_{i}"),
319 )
320 .await?;
321 }
322
323 clear(&remote_repo).await?;
325
326 let workspaces = list(&remote_repo).await?;
328 assert_eq!(workspaces.len(), 0);
329
330 Ok(remote_repo)
331 })
332 .await
333 }
334
335 #[tokio::test]
336 async fn test_list_workspaces() -> Result<(), OxenError> {
337 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
338 let branch_name = "main";
339 create(&remote_repo, branch_name, "test_workspace_id").await?;
340 create(&remote_repo, branch_name, "test_workspace_id2").await?;
341
342 let workspaces = list(&remote_repo).await?;
343 assert_eq!(workspaces.len(), 2);
344
345 Ok(remote_repo)
346 })
347 .await
348 }
349
350 #[tokio::test]
351 async fn test_list_empty_workspaces() -> Result<(), OxenError> {
352 test::run_empty_remote_repo_test(|_local_repo, remote_repo| async move {
353 let workspaces = list(&remote_repo).await?;
354 assert_eq!(workspaces.len(), 0);
355
356 Ok(remote_repo)
357 })
358 .await
359 }
360
361 #[tokio::test]
362 async fn test_delete_workspace() -> Result<(), OxenError> {
363 test::run_readme_remote_repo_test(|_local_repo, remote_repo| async move {
364 let branch_name = "main";
365 let workspace_id = "test_workspace_id";
366 let workspace = create(&remote_repo, branch_name, workspace_id).await?;
367
368 assert_eq!(workspace.id, workspace_id);
369
370 let res = delete(&remote_repo, workspace_id).await;
371 assert!(res.is_ok());
372
373 Ok(remote_repo)
374 })
375 .await
376 }
377
378 #[tokio::test]
379 async fn test_remote_commit_fails_if_schema_changed() -> Result<(), OxenError> {
380 if std::env::consts::OS == "windows" {
382 return Ok(());
383 }
384
385 test::run_training_data_fully_sync_remote(|_, remote_repo| async move {
386 let remote_repo_copy = remote_repo.clone();
387
388 test::run_empty_dir_test_async(|repo_dir| async move {
389 let cloned_repo =
390 repositories::clone_url(&remote_repo.remote.url, &repo_dir.join("new_repo"))
391 .await?;
392
393 let path = test::test_nlp_classification_csv();
395
396 let workspace_id = "my_workspace";
398 api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, &workspace_id)
399 .await?;
400
401 repositories::workspaces::df::index(&cloned_repo, workspace_id, &path).await?;
403
404 log::debug!("the path in question is {path:?}");
405 let mut opts = DFOpts::empty();
406
407 opts.add_row =
408 Some("{\"text\": \"I am a new row\", \"label\": \"neutral\"}".to_string());
409 repositories::workspaces::df(&cloned_repo, workspace_id, &path, opts).await?;
410
411 let full_path = cloned_repo.path.join(path);
413 let mut opts = DFOpts::empty();
414 opts.add_col = Some("is_something:n/a:str".to_string());
415 opts.output = Some(full_path.to_path_buf()); command::df(&full_path, opts).await?;
417 repositories::add(&cloned_repo, &full_path).await?;
418
419 repositories::commit(&cloned_repo, "Changed the schema 😇")?;
421 repositories::push(&cloned_repo).await?;
422
423 let body = NewCommitBody {
425 message: "Remotely committing".to_string(),
426 author: "Test User".to_string(),
427 email: "test@oxen.ai".to_string(),
428 };
429 let result = api::client::workspaces::commit(
430 &remote_repo,
431 DEFAULT_BRANCH_NAME,
432 workspace_id,
433 &body,
434 )
435 .await;
436 assert!(result.is_err());
437
438 let remote_status = api::client::workspaces::changes::list(
440 &remote_repo,
441 &workspace_id,
442 Path::new(""),
443 constants::DEFAULT_PAGE_NUM,
444 constants::DEFAULT_PAGE_SIZE,
445 )
446 .await?;
447 assert_eq!(remote_status.modified_files.entries.len(), 1);
448
449 Ok(())
450 })
451 .await?;
452
453 Ok(remote_repo_copy)
454 })
455 .await
456 }
457
458 #[tokio::test]
459 async fn test_remote_commit_staging_behind_main() -> Result<(), OxenError> {
460 test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
461 let new_branch = "behind-main";
463 let main_branch = "main";
464
465 let main_path = "images/folder";
466 let workspace =
467 api::client::workspaces::create(&remote_repo, main_branch, "test_workspace")
468 .await?;
469 let identifier = workspace.id;
470
471 api::client::branches::create_from_branch(&remote_repo, new_branch, main_branch)
472 .await?;
473
474 let path = test::test_img_file();
476 let result = api::client::workspaces::files::upload_single_file(
477 &remote_repo,
478 &identifier,
479 main_path,
480 path,
481 )
482 .await;
483 assert!(result.is_ok());
484
485 let body = NewCommitBody {
486 message: "Add to main".to_string(),
487 author: "Test User".to_string(),
488 email: "test@oxen.ai".to_string(),
489 };
490 api::client::workspaces::commit(&remote_repo, main_branch, &identifier, &body).await?;
491
492 let workspace =
493 api::client::workspaces::create(&remote_repo, new_branch, "test_workspace").await?;
494 let identifier = workspace.id;
495
496 let image_path = test::test_1k_parquet();
498 let result = api::client::workspaces::files::upload_single_file(
499 &remote_repo,
500 &identifier,
501 main_path,
502 image_path,
503 )
504 .await;
505 assert!(result.is_ok());
506
507 let body = NewCommitBody {
509 message: "Add behind main".to_string(),
510 author: "Test User".to_string(),
511 email: "test@oxen.ai".to_string(),
512 };
513 api::client::workspaces::commit(&remote_repo, new_branch, &identifier, &body).await?;
514
515 let workspace =
516 api::client::workspaces::create(&remote_repo, new_branch, "test_workspace").await?;
517 let identifier = workspace.id;
518
519 let image_path = test::test_100_parquet();
521 let result = api::client::workspaces::files::upload_single_file(
522 &remote_repo,
523 &identifier,
524 main_path,
525 image_path,
526 )
527 .await;
528 assert!(result.is_ok());
529
530 let page_num = constants::DEFAULT_PAGE_NUM;
532 let page_size = constants::DEFAULT_PAGE_SIZE;
533 let path = Path::new("");
534
535 let remote_status = api::client::workspaces::changes::list(
536 &remote_repo,
537 &identifier,
538 path,
539 page_num,
540 page_size,
541 )
542 .await?;
543
544 assert_eq!(remote_status.added_files.entries.len(), 1);
545 assert_eq!(remote_status.added_files.total_entries, 1);
546
547 Ok(remote_repo)
548 })
549 .await
550 }
551
552 #[tokio::test]
553 async fn test_not_named_workspaces_closing_after_commit() -> Result<(), OxenError> {
554 test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
555 let workspace_id = "test_workspace_id";
556 api::client::workspaces::create(&remote_repo, DEFAULT_BRANCH_NAME, workspace_id)
557 .await?;
558 let path = test::test_img_file();
559 let result = api::client::workspaces::files::upload_single_file(
560 &remote_repo,
561 &&workspace_id,
562 "",
563 path,
564 )
565 .await;
566 assert!(result.is_ok());
567
568 let body = NewCommitBody {
569 message: "Add to main".to_string(),
570 author: "Test User".to_string(),
571 email: "test@oxen.ai".to_string(),
572 };
573 api::client::workspaces::commit(&remote_repo, DEFAULT_BRANCH_NAME, workspace_id, &body)
574 .await?;
575 let get_result = api::client::workspaces::get(&remote_repo, &workspace_id).await?;
576
577 assert!(get_result.is_none());
578
579 Ok(remote_repo)
580 })
581 .await
582 }
583
584 #[tokio::test]
585 async fn test_named_workspaces_not_closing_after_commit() -> Result<(), OxenError> {
586 test::run_remote_repo_test_bounding_box_csv_pushed(|_local_repo, remote_repo| async move {
587 let workspace_name = "test_workspace_name";
588 let workspace_id = "test_workspace_id";
589 api::client::workspaces::create_with_name(
590 &remote_repo,
591 DEFAULT_BRANCH_NAME,
592 workspace_id,
593 workspace_name,
594 )
595 .await?;
596 let path = test::test_img_file();
597 let result = api::client::workspaces::files::upload_single_file(
598 &remote_repo,
599 &workspace_name,
600 "",
601 path,
602 )
603 .await;
604 assert!(result.is_ok());
605
606 let body = NewCommitBody {
607 message: "Add to main".to_string(),
608 author: "Test User".to_string(),
609 email: "test@oxen.ai".to_string(),
610 };
611 api::client::workspaces::commit(
612 &remote_repo,
613 DEFAULT_BRANCH_NAME,
614 workspace_name,
615 &body,
616 )
617 .await?;
618 let workspace = api::client::workspaces::get(&remote_repo, &workspace_name).await?;
619 assert!(workspace.is_some());
620 assert_eq!(workspace.as_ref().unwrap().id, workspace_id);
621 Ok(remote_repo)
622 })
623 .await
624 }
625}