From 176ff70d18353354b2cd96dc14d2d03ad173dfd2 Mon Sep 17 00:00:00 2001 From: Federico Ravasio Date: Mon, 2 Mar 2026 12:44:36 +0000 Subject: [PATCH] feat(aerocloud v7): reusable models support in `batch` command --- CHANGELOG.md | 1 + README.md | 10 + build.rs | 10 +- src/aerocloud/extra_types.rs | 5 +- src/commands/aerocloud/v7/batch.rs | 109 ++++++-- .../aerocloud/v7/batch/simulation_detail.rs | 156 +++++++++++- .../aerocloud/v7/batch/simulation_params.rs | 232 +++++++++++------- src/commands/aerocloud/v7/batch/submit.rs | 69 ++++-- 8 files changed, 447 insertions(+), 145 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c5c6dee..a3730cc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - Add `v7 list-reusable-models` command. [#146](https://github.com/nablaflow/cli/pull/146) + - Add support for reusable models in `v7 batch`. # 1.1.0 - 2026-01-28 diff --git a/README.md b/README.md index 252351c..3826343 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,16 @@ dir An example is available under [examples/aerocloud/v7/batch](examples/aerocloud/v7/batch). +> [!TIP] +> You can skip uploading files and use a reusable model by just setting its ID in each simulation's `params.json`: +> +> ```json +> { +> "model_id": "b7203095-f9fd-4270-ac6d-03c46b02932b", +> ... +> } +> ``` + With the folder ready, run the following command to enter the Batch mode: ```bash diff --git a/build.rs b/build.rs index 082119c..2882c57 100644 --- a/build.rs +++ b/build.rs @@ -5,7 +5,7 @@ use std::{ path::Path, }; use syn::{ - ItemEnum, + ItemEnum, ItemStruct, visit_mut::{self, VisitMut}, }; @@ -47,4 +47,12 @@ impl VisitMut for AddClapValueEnum { visit_mut::visit_item_enum_mut(self, node); } + + fn visit_item_struct_mut(&mut self, node: &mut ItemStruct) { + if node.ident == "Quaternion" { + node.attrs.push(syn::parse_quote! { #[derive(PartialEq)] }); + } + + visit_mut::visit_item_struct_mut(self, node); + } } diff --git a/src/aerocloud/extra_types.rs b/src/aerocloud/extra_types.rs index 4686f89..5e9cb5d 100644 --- a/src/aerocloud/extra_types.rs +++ b/src/aerocloud/extra_types.rs @@ -1,5 +1,5 @@ use crate::aerocloud::types::{ - BoundaryLayerTreatment, FileUnit, Fluid, FluidSpeed, GroundOffset, + BoundaryLayerTreatment, FileUnit, Fluid, FluidSpeed, GroundOffset, Id, Quaternion, SimulationQuality, UpdatePartV7Params, YawAngle, YawAngles, }; use color_eyre::eyre; @@ -36,6 +36,9 @@ pub struct CreateSimulationV7ParamsFromJson { #[serde(default)] pub boundary_layer_treatment: Option, + + #[serde(default)] + pub model_id: Option, } impl CreateSimulationV7ParamsFromJson { diff --git a/src/commands/aerocloud/v7/batch.rs b/src/commands/aerocloud/v7/batch.rs index be3a00c..5d35500 100644 --- a/src/commands/aerocloud/v7/batch.rs +++ b/src/commands/aerocloud/v7/batch.rs @@ -65,7 +65,7 @@ const SLEEP_FOR_FEEDBACK: Duration = Duration::from_millis(100); pub async fn run(client: &Client, root_dir: Option<&Path>) -> eyre::Result<()> { let sims = if let Some(root_dir) = root_dir { - let sims = SimulationParams::many_from_root_dir(root_dir).await?; + let sims = SimulationParams::many_from_root_dir(client, root_dir).await?; if sims.is_empty() { tracing::error!("no simulations found in `{}`", root_dir.display()); @@ -89,7 +89,11 @@ pub async fn run(client: &Client, root_dir: Option<&Path>) -> eyre::Result<()> { result } -pub fn refresh_sims_in_background(root_dir: &Path, tx: mpsc::Sender) { +pub fn refresh_sims_in_background( + client: Client, + root_dir: &Path, + tx: mpsc::Sender, +) { let root_dir = root_dir.to_owned(); tokio::spawn(async move { @@ -97,8 +101,8 @@ pub fn refresh_sims_in_background(root_dir: &Path, tx: mpsc::Sender) { // operation. time::sleep(SLEEP_FOR_FEEDBACK).await; - let sims = SimulationParams::many_from_root_dir(&root_dir).await?; - tx.send(Event::SimsReloaded(sims)).await?; + let res = SimulationParams::many_from_root_dir(&client, &root_dir).await; + tx.send(Event::SimsReloaded(res)).await?; Ok::<(), eyre::Report>(()) }); @@ -126,6 +130,7 @@ enum ActiveState { }, ConfirmSubmit, ReloadingSims, + ReloadingSimsFailed(String), Submitting { cancellation_token: CancellationToken, bytes_count: ByteSize, @@ -157,7 +162,7 @@ pub enum Event { ProjectsUpdated(eyre::Result>), ProjectSelected(Box), FileUploaded(ByteSize), - SimsReloaded(Vec), + SimsReloaded(eyre::Result>), SimSubmitted { internal_id: Uuid, res: eyre::Result, eyre::Report>, @@ -327,7 +332,11 @@ impl Batch { } (KeyCode::Char('r'), _) => { if let Some(root_dir) = self.root_dir.as_ref() { - refresh_sims_in_background(root_dir, tx.clone()); + refresh_sims_in_background( + self.client.clone(), + root_dir, + tx.clone(), + ); } next_state = Some(ActiveState::ReloadingSims); @@ -374,7 +383,11 @@ impl Batch { } (KeyCode::Char('r'), _) => { if let Some(root_dir) = self.root_dir.as_ref() { - refresh_sims_in_background(root_dir, tx.clone()); + refresh_sims_in_background( + self.client.clone(), + root_dir, + tx.clone(), + ); } next_state = Some(ActiveState::ReloadingSims); @@ -469,14 +482,14 @@ impl Batch { _ => {} } } - (ActiveState::ReloadingSims, Event::KeyPressed(key_event)) => { - if let KeyCode::Char('q') = key_event.code { - next_state = Some(ActiveState::ViewingList); - } + (ActiveState::ReloadingSims, Event::KeyPressed(key_event)) + if key_event.code == KeyCode::Char('q') => + { + next_state = Some(ActiveState::ViewingList); } ( ActiveState::ReloadingSims, - Event::SimsReloaded(mut simulations), + Event::SimsReloaded(Ok(mut simulations)), ) => { // Copy over selection status. for new_sim in &mut simulations { @@ -490,13 +503,24 @@ impl Batch { self.simulations = simulations; next_state = Some(ActiveState::ViewingList); } + (ActiveState::ReloadingSims, Event::SimsReloaded(Err(err))) => { + next_state = Some(ActiveState::ReloadingSimsFailed( + human_err_report(&err), + )); + } + ( + ActiveState::ReloadingSimsFailed(..), + Event::KeyPressed(key_event), + ) if key_event.code == KeyCode::Char('q') => { + next_state = Some(ActiveState::ViewingList); + } ( ActiveState::Submitting { cancellation_token, .. }, Event::KeyPressed(key_event), ) => { - if let KeyCode::Char('q') = key_event.code { + if key_event.code == KeyCode::Char('q') { cancellation_token.cancel(); // TODO: should we ask for confirmation? @@ -618,13 +642,16 @@ impl Batch { match state { ActiveState::ReloadingSims => { - Batch::render_reloading_sims_popup(area, buf); + Self::render_reloading_sims_popup(area, buf); + } + ActiveState::ReloadingSimsFailed(error) => { + Self::render_reloading_sims_failed_popup(area, buf, error); } ActiveState::ConfirmExit { .. } => { - Batch::render_exit_popup(area, buf); + Self::render_exit_popup(area, buf); } ActiveState::ConfirmSubmit => { - Batch::render_submit_confirmation_popup(simulations, area, buf); + Self::render_submit_confirmation_popup(simulations, area, buf); } ActiveState::Submitting { bytes_count, @@ -633,10 +660,10 @@ impl Batch { sims_progress, .. } => { - assert!(*bytes_count > ByteSize::default()); + assert!(*bytes_count >= ByteSize::default()); assert!(*sims_count > 0); - Batch::render_submitting( + Self::render_submitting( *bytes_count, *bytes_progress, *sims_count, @@ -767,6 +794,44 @@ impl Batch { Widget::render(¶graph, area, buf); } + fn render_reloading_sims_failed_popup( + area: Rect, + buf: &mut Buffer, + error: &str, + ) { + let lines = { + let mut l = vec![Line::default()]; + + for line in error.lines() { + l.push(Line::from(line)); + } + + l.push(Line::default()); + + l + }; + + let area = center( + area, + Constraint::Percentage(55), + Constraint::Length(u16::try_from(lines.len()).unwrap_or(5) + 2), // top and bottom border + content + ); + + let block = Block::bordered() + .title( + Line::from(Span::styled(" Failed to reload config ", STYLE_BOLD)) + .centered(), + ) + .title_bottom(Line::raw(" (q) close and continue ").centered()) + .border_set(border::THICK) + .style(STYLE_ERROR); + + let paragraph = Paragraph::new(lines).block(block); + + Widget::render(&Clear, area, buf); + Widget::render(¶graph, area, buf); + } + fn render_submit_confirmation_popup( simulations: &[SimulationParams], area: Rect, @@ -911,7 +976,11 @@ impl Batch { .padding(Padding::vertical(1)) .title(Line::from("Uploading files").centered()), ) - .ratio(bytes_progress.0 as f64 / bytes_count.0 as f64) + .ratio(if bytes_count.0 == 0 { + 1.0 + } else { + bytes_progress.0 as f64 / bytes_count.0 as f64 + }) .label(Span::styled( format!("{bytes_progress}/{bytes_count}"), Style::new().bold(), @@ -1022,7 +1091,7 @@ impl From<&SimulationParams> for ListItem<'_> { } } - if p.files.is_empty() { + if p.model_params.is_empty() { spans.push(Span::styled("(no files) ", STYLE_ERROR)); } diff --git a/src/commands/aerocloud/v7/batch/simulation_detail.rs b/src/commands/aerocloud/v7/batch/simulation_detail.rs index 957a6e5..f0dc19b 100644 --- a/src/commands/aerocloud/v7/batch/simulation_detail.rs +++ b/src/commands/aerocloud/v7/batch/simulation_detail.rs @@ -1,9 +1,14 @@ use crate::{ - aerocloud::fmt, + aerocloud::{ + fmt, + types::{ModelV7, ModelV7FilesItem, Quaternion}, + }, commands::aerocloud::v7::batch::{ STYLE_ACCENT, STYLE_BOLD, STYLE_DIMMED, STYLE_ERROR, STYLE_NORMAL, STYLE_SUCCESS, STYLE_WARNING, - simulation_params::{FileParams, SimulationParams, SubmissionState}, + simulation_params::{ + FileParams, ModelParams, SimulationParams, SubmissionState, + }, }, }; use itertools::Itertools; @@ -171,8 +176,13 @@ impl<'a> SimulationDetail<'a> { } } - fn files_lines(sim: &'a SimulationParams, lines: &mut Vec>) { - let files = &sim.files; + fn new_model(files: &'a [FileParams], lines: &mut Vec>) { + lines.push(Line::from(vec![Span::styled( + "New model to upload.", + STYLE_BOLD, + )])); + + lines.push(Line::default()); if files.is_empty() { lines.push(Line::styled( @@ -222,11 +232,58 @@ impl<'a> SimulationDetail<'a> { lines.push(Line::default()); - Self::parts_lines(file, lines); + Self::new_parts_lines(file, lines); } } - fn parts_lines(file: &'a FileParams, lines: &mut Vec>) { + fn existing_model(model: &'a ModelV7, lines: &mut Vec>) { + lines.push(Line::from(vec![ + Span::styled("Reusable model: ", STYLE_BOLD), + Span::styled(model.name.clone(), STYLE_ACCENT), + ])); + + lines.push(Line::default()); + + lines.push(Line::from(vec![ + Span::styled("Files ", STYLE_BOLD), + Span::styled(format!("({})", model.files.len()), STYLE_ACCENT), + Span::styled(":", STYLE_BOLD), + ])); + + for file in &model.files { + lines.push(Line::default()); + + lines.push(Line::from(vec![ + Span::raw(" - "), + Span::styled("Name: ", STYLE_BOLD), + Span::styled((*file.name).clone(), STYLE_ACCENT), + ])); + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Unit: ", STYLE_BOLD), + Span::styled(format!("{}", file.unit), STYLE_ACCENT), + ])); + + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Rotation: ", STYLE_BOLD), + if file.rotation == Quaternion([1.0, 0.0, 0.0, 0.0]) { + Span::styled( + format!("{:?} (quaternion)", file.rotation.0), + STYLE_ACCENT, + ) + } else { + Span::styled("none", STYLE_ACCENT) + }, + ])); + + lines.push(Line::default()); + + Self::existing_parts_lines(file, lines); + } + } + + fn new_parts_lines(file: &'a FileParams, lines: &mut Vec>) { let parts = &file.params.parts; if parts.is_empty() { @@ -303,6 +360,84 @@ impl<'a> SimulationDetail<'a> { } } } + + fn existing_parts_lines( + file: &'a ModelV7FilesItem, + lines: &mut Vec>, + ) { + let parts = &file.parts; + + if parts.is_empty() { + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled( + "No parts configured. What the file contains will be used as is.", + STYLE_WARNING + ), + ])); + + return; + } + + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Parts ", STYLE_BOLD), + Span::styled(format!("({})", parts.len()), STYLE_ACCENT), + Span::styled(":", STYLE_BOLD), + ])); + + for part in parts { + lines.push(Line::default()); + + lines.push(Line::from(vec![ + Span::raw(" - "), + Span::styled("Name: ", STYLE_BOLD), + Span::styled(part.name.clone(), STYLE_ACCENT), + ])); + + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Rolling: ", STYLE_BOLD), + Span::styled(bool_to_human(part.rolling), STYLE_ACCENT), + ])); + + let is_porous = part.is_porous.unwrap_or(false); + + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Porous: ", STYLE_BOLD), + Span::styled(bool_to_human(is_porous), STYLE_ACCENT), + ])); + + if is_porous { + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Darcy coeff: ", STYLE_BOLD), + if let Some(darcy_coeff) = &part.darcy_coeff { + Span::styled(format!("{darcy_coeff}"), STYLE_ACCENT) + } else { + Span::styled( + " (required when part is marked as porous)", + STYLE_ERROR + ) + } + ])); + + lines.push(Line::from(vec![ + Span::raw(" "), + Span::styled("Forchheimer coeff: ", STYLE_BOLD), + if let Some(forchheimer_coeff) = &part.forchheimer_coeff { + Span::styled(format!("{forchheimer_coeff}"), STYLE_ACCENT) + } else { + Span::styled( + " (required when part is marked as porous)", + STYLE_ERROR + ) + } + ])); + } + } + } } impl StatefulWidget for &SimulationDetail<'_> { @@ -332,7 +467,14 @@ impl StatefulWidget for &SimulationDetail<'_> { lines.push(Line::default()); - SimulationDetail::files_lines(sim, &mut lines); + match &sim.model_params { + ModelParams::New { files } => { + SimulationDetail::new_model(files, &mut lines); + } + ModelParams::Existing { model } => { + SimulationDetail::existing_model(model, &mut lines); + } + } *scrollbar_state = scrollbar_state.content_length(lines.len()); diff --git a/src/commands/aerocloud/v7/batch/simulation_params.rs b/src/commands/aerocloud/v7/batch/simulation_params.rs index ffe3aa4..f33169e 100644 --- a/src/commands/aerocloud/v7/batch/simulation_params.rs +++ b/src/commands/aerocloud/v7/batch/simulation_params.rs @@ -1,8 +1,9 @@ use crate::aerocloud::{ + Client, extra_types::{CreateSimulationV7ParamsFromJson, FileV7ParamsFromJson}, types::{ CreateModelV7Params, CreateModelV7ParamsFilesItem, - CreateSimulationV7Params, Filename, Id, Url, + CreateSimulationV7Params, Filename, Id, ModelV7, Url, }, }; use bytesize::ByteSize; @@ -43,90 +44,27 @@ impl SubmissionState { } #[derive(Debug, Clone)] -pub struct SimulationParams { - pub internal_id: Uuid, - pub dir: PathBuf, - pub params: CreateSimulationV7ParamsFromJson, - pub files: Vec, - - pub selected: bool, - pub submission_state: SubmissionState, +pub enum ModelParams { + New { files: Vec }, + Existing { model: ModelV7 }, } -impl SimulationParams { - pub async fn many_from_root_dir(root_dir: &Path) -> eyre::Result> { - if !fs::metadata(root_dir).await?.is_dir() { - eyre::bail!("`{}` is not a directory", root_dir.display()); +impl ModelParams { + pub fn is_empty(&self) -> bool { + match self { + Self::New { files } => files.is_empty(), + Self::Existing { model } => model.files.is_empty(), } + } - let mut sims_params = vec![]; - - let mut dir_stream = - fs::read_dir(root_dir).await.wrap_err_with(|| { - eyre::eyre!("error listing root dir `{}`", root_dir.display()) - })?; - - while let Some(entry) = dir_stream - .next_entry() - .await - .wrap_err("iterating root dir dir stream")? - { - let path = entry.path(); - - if !path.is_dir() { - continue; - } - - sims_params.push(Self::from_dir(&path).await.wrap_err_with( - || { - format!( - "failed to build simulation params from dir `{}`", - path.display() - ) - }, - )?); + pub fn is_submittable(&self) -> bool { + match self { + Self::New { files } => !files.is_empty(), + Self::Existing { .. } => true, } - - Ok(sims_params) } - #[allow(clippy::too_many_lines)] - pub async fn from_dir(dir: &Path) -> eyre::Result { - let params_path = dir.join("params.json"); - - let dir_name = dir.file_name().ok_or_else(|| { - eyre::eyre!("no file name for path `{}`", dir.display()) - })?; - let sim_name = dir_name - .to_str() - .ok_or_else(|| { - eyre::eyre!( - "dir name {:?} contains invalid utf-8 characters", - dir_name - ) - })? - .to_owned(); - - let params = if params_path.exists() { - let buf = fs::read(¶ms_path).await.wrap_err_with(|| { - format!("failed to read `{}`", params_path.display()) - })?; - - let mut params: CreateSimulationV7ParamsFromJson = - serde_json::from_slice(&buf).wrap_err_with(|| { - format!("failed to parse `{}`", params_path.display()) - })?; - - params.name = sim_name; - - params - } else { - CreateSimulationV7ParamsFromJson { - name: sim_name, - ..Default::default() - } - }; - + async fn from_dir(dir: &Path) -> eyre::Result { let mut files = vec![]; let mut dir_stream = fs::read_dir(dir) @@ -203,20 +141,132 @@ impl SimulationParams { files.sort_unstable_by(|a, b| a.path.cmp(&b.path)); + Ok(Self::New { files }) + } + + async fn from_existing(client: &Client, id: &Id) -> eyre::Result { + let model = client + .models_v7_get(id) + .await + .wrap_err_with(|| format!("fetching reusable model {id}"))? + .into_inner(); + + Ok(Self::Existing { model }) + } +} + +#[derive(Debug, Clone)] +pub struct SimulationParams { + pub internal_id: Uuid, + pub dir: PathBuf, + pub params: CreateSimulationV7ParamsFromJson, + pub model_params: ModelParams, + + pub selected: bool, + pub submission_state: SubmissionState, +} + +impl SimulationParams { + pub async fn many_from_root_dir( + client: &Client, + root_dir: &Path, + ) -> eyre::Result> { + if !fs::metadata(root_dir).await?.is_dir() { + eyre::bail!("`{}` is not a directory", root_dir.display()); + } + + let mut sims_params = vec![]; + + let mut dir_stream = + fs::read_dir(root_dir).await.wrap_err_with(|| { + eyre::eyre!("error listing root dir `{}`", root_dir.display()) + })?; + + while let Some(entry) = dir_stream + .next_entry() + .await + .wrap_err("iterating root dir dir stream")? + { + let path = entry.path(); + + if !path.is_dir() { + continue; + } + + sims_params.push(Self::from_dir(client, &path).await.wrap_err_with( + || { + format!( + "failed to build simulation params from dir `{}`", + path.display() + ) + }, + )?); + } + + Ok(sims_params) + } + + #[allow(clippy::too_many_lines)] + pub async fn from_dir(client: &Client, dir: &Path) -> eyre::Result { + let params_path = dir.join("params.json"); + + let dir_name = dir.file_name().ok_or_else(|| { + eyre::eyre!("no file name for path `{}`", dir.display()) + })?; + let sim_name = dir_name + .to_str() + .ok_or_else(|| { + eyre::eyre!( + "dir name {:?} contains invalid utf-8 characters", + dir_name + ) + })? + .to_owned(); + + let params = if params_path.exists() { + let buf = fs::read(¶ms_path).await.wrap_err_with(|| { + format!("failed to read `{}`", params_path.display()) + })?; + + let mut params: CreateSimulationV7ParamsFromJson = + serde_json::from_slice(&buf).wrap_err_with(|| { + format!("failed to parse `{}`", params_path.display()) + })?; + + params.name = sim_name; + + params + } else { + CreateSimulationV7ParamsFromJson { + name: sim_name, + ..Default::default() + } + }; + + let model_params = if let Some(model_id) = ¶ms.model_id { + ModelParams::from_existing(client, model_id).await? + } else { + ModelParams::from_dir(dir).await? + }; + let submission_state = SubmissionState::from_dir_or_default(dir).await; Ok(Self { internal_id: Uuid::new_v4(), dir: dir.into(), params, - files, + model_params, selected: true, submission_state, }) } pub fn files_size(&self) -> ByteSize { - self.files + let ModelParams::New { files } = &self.model_params else { + return ByteSize::default(); + }; + + files .iter() .fold(ByteSize::default(), |acc, file| acc + file.size) } @@ -237,27 +287,30 @@ impl SimulationParams { pub fn is_submittable(&self) -> bool { self.selected - && !self.files.is_empty() + && self.model_params.is_submittable() && matches!( self.submission_state, SubmissionState::Ready | SubmissionState::Error(..) ) } - pub fn into_api_model_params(self) -> CreateModelV7Params { - CreateModelV7Params { + pub fn into_api_create_model_params(self) -> Option { + let ModelParams::New { files } = self.model_params else { + return None; + }; + + Some(CreateModelV7Params { name: self.params.name.clone(), reusable: false, - files: self - .files - .iter() + files: files + .into_iter() .map(|file| CreateModelV7ParamsFilesItem { - name: file.filename.clone(), - rotation: file.params.rotation.clone(), + name: file.filename, + rotation: file.params.rotation, unit: file.params.unit, }) .collect(), - } + }) } pub fn into_api_params( @@ -276,6 +329,7 @@ impl SimulationParams { quality, revision, yaw_angles, + .. } = self.params; CreateSimulationV7Params { diff --git a/src/commands/aerocloud/v7/batch/submit.rs b/src/commands/aerocloud/v7/batch/submit.rs index b6a6231..ac72751 100644 --- a/src/commands/aerocloud/v7/batch/submit.rs +++ b/src/commands/aerocloud/v7/batch/submit.rs @@ -5,7 +5,7 @@ use crate::{ }, commands::aerocloud::v7::batch::{ Event, - simulation_params::{FileParams, SimulationParams}, + simulation_params::{FileParams, ModelParams, SimulationParams}, }, http::UPLOAD_REQ_TIMEOUT, }; @@ -55,32 +55,7 @@ async fn submit_sim( client: Client, tx: mpsc::Sender, ) -> eyre::Result { - let ModelV7 { - id: model_id, - files, - .. - } = client - .models_v7_create( - &new_idempotency_key(), - &sim.clone().into_api_model_params(), - ) - .await - .map_err(fmt_progenitor_err)? - .into_inner(); - - upload_files(&client, &files, &sim.files, &tx) - .await - .wrap_err("uploading files")?; - - let ModelV7 { files, .. } = client - .models_v7_finalise(&model_id, &new_idempotency_key()) - .await - .map_err(fmt_progenitor_err)? - .into_inner(); - - update_parts(&client, &model_id, &files, &sim.files) - .await - .wrap_err("updating parts")?; + let model_id = submit_model_if_needed(&client, &sim, tx).await?; let sim = client .simulations_v7_create( @@ -94,6 +69,46 @@ async fn submit_sim( Ok(sim) } +async fn submit_model_if_needed( + client: &Client, + sim: &SimulationParams, + tx: mpsc::Sender, +) -> eyre::Result { + match &sim.model_params { + ModelParams::Existing { model } => Ok(model.id.clone()), + ModelParams::New { files: model_files } => { + let ModelV7 { + id: model_id, + files, + .. + } = client + .models_v7_create( + &new_idempotency_key(), + &sim.clone().into_api_create_model_params().unwrap(), + ) + .await + .map_err(fmt_progenitor_err)? + .into_inner(); + + upload_files(client, &files, model_files, &tx) + .await + .wrap_err("uploading files")?; + + let ModelV7 { files, .. } = client + .models_v7_finalise(&model_id, &new_idempotency_key()) + .await + .map_err(fmt_progenitor_err)? + .into_inner(); + + update_parts(client, &model_id, &files, model_files) + .await + .wrap_err("updating parts")?; + + Ok(model_id) + } + } +} + async fn upload_files( client: &Client, files: &[ModelV7FilesItem],