Skip to content

Commit 97cf430

Browse files
committed
Fix report analysis input paths
1 parent 00cba8c commit 97cf430

3 files changed

Lines changed: 297 additions & 238 deletions

File tree

‎rust/bioscript-cli/src/main.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ include!("cli_commands.rs");
66
include!("report_options.rs");
77
include!("package.rs");
88
include!("report_review.rs");
9+
include!("report_analysis_runtime.rs");
910
include!("report_execution.rs");
1011
include!("report_output.rs");
1112
include!("manifest_runner.rs");
Lines changed: 295 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,295 @@
1+
struct BioscriptAnalysisScriptInput<'a> {
2+
runtime_root: &'a Path,
3+
manifest_path: &'a Path,
4+
interpretation: &'a PanelInterpretation,
5+
script_path: &'a Path,
6+
input_file: &'a Path,
7+
output_file: &'a Path,
8+
observations_file: &'a Path,
9+
participant_id: &'a str,
10+
loader: &'a GenotypeLoadOptions,
11+
analysis_max_duration_ms: u64,
12+
}
13+
14+
fn run_bioscript_analysis_script(input: &BioscriptAnalysisScriptInput<'_>) -> Result<(), String> {
15+
let limits = ResourceLimits::new()
16+
.max_duration(Duration::from_millis(input.analysis_max_duration_ms))
17+
.max_memory(16 * 1024 * 1024)
18+
.max_allocations(400_000)
19+
.gc_interval(1000)
20+
.max_recursion_depth(Some(200));
21+
let observations_text = fs::read_to_string(input.observations_file).map_err(|err| {
22+
format!(
23+
"failed to read analysis observations {}: {err}",
24+
input.observations_file.display()
25+
)
26+
})?;
27+
let script_text = fs::read_to_string(input.script_path).map_err(|err| {
28+
format!(
29+
"failed to read analysis script {}: {err}",
30+
input.script_path.display()
31+
)
32+
})?;
33+
let virtual_input_file = "/input/genotypes".to_owned();
34+
let virtual_observations_file = "/work/observations.tsv".to_owned();
35+
let output_extension = input
36+
.output_file
37+
.extension()
38+
.and_then(|value| value.to_str())
39+
.unwrap_or("tsv");
40+
let virtual_output_file = format!("/output/results.{output_extension}");
41+
let (analysis_input_file, virtual_input_bytes) =
42+
analysis_input_file_arg(input.runtime_root, input.input_file)?;
43+
let script_virtual_path = virtual_pipeline_path(input.script_path, "analysis.py");
44+
let manifest_virtual_path = virtual_pipeline_path(input.manifest_path, "manifest.yaml");
45+
let AnalysisVirtualTextFiles {
46+
text_files: virtual_text_files,
47+
asset_paths,
48+
} = collect_analysis_virtual_text_files(
49+
input,
50+
&script_virtual_path,
51+
script_text,
52+
&manifest_virtual_path,
53+
&virtual_observations_file,
54+
observations_text,
55+
)?;
56+
let mut virtual_binary_files = BTreeMap::new();
57+
if let Some(input_bytes) = virtual_input_bytes {
58+
virtual_binary_files.insert(virtual_input_file.clone(), input_bytes);
59+
}
60+
let context = analysis_context(
61+
input.participant_id,
62+
&analysis_input_file,
63+
&script_virtual_path,
64+
&manifest_virtual_path,
65+
&asset_paths,
66+
&virtual_observations_file,
67+
&virtual_output_file,
68+
);
69+
let runtime = BioscriptRuntime::with_config(
70+
input.runtime_root.to_path_buf(),
71+
RuntimeConfig {
72+
limits,
73+
loader: input.loader.clone(),
74+
context,
75+
virtual_binary_files,
76+
virtual_text_files,
77+
..RuntimeConfig::default()
78+
},
79+
)
80+
.map_err(|err| err.to_string())?;
81+
runtime
82+
.run_file(
83+
&script_virtual_path,
84+
None,
85+
vec![
86+
("input_file", monty::MontyObject::String(analysis_input_file)),
87+
(
88+
"output_file",
89+
monty::MontyObject::String(virtual_output_file.clone()),
90+
),
91+
(
92+
"observations_file",
93+
monty::MontyObject::String(virtual_observations_file),
94+
),
95+
("asset_paths", monty_string_dict(&asset_paths)),
96+
(
97+
"participant_id",
98+
monty::MontyObject::String(input.participant_id.to_owned()),
99+
),
100+
],
101+
)
102+
.map_err(|err| err.to_string())?;
103+
let written = runtime.virtual_written_text_files();
104+
let output_text = written
105+
.get(&virtual_output_file)
106+
.ok_or_else(|| format!("analysis did not write {virtual_output_file}"))?;
107+
if let Some(parent) = input.output_file.parent() {
108+
fs::create_dir_all(parent).map_err(|err| {
109+
format!(
110+
"failed to create analysis output dir {}: {err}",
111+
parent.display()
112+
)
113+
})?;
114+
}
115+
fs::write(input.output_file, output_text).map_err(|err| {
116+
format!(
117+
"failed to write analysis output {}: {err}",
118+
input.output_file.display()
119+
)
120+
})?;
121+
persist_virtual_output_files(&written, &virtual_output_file, input.output_file)
122+
}
123+
124+
fn analysis_input_file_arg(
125+
runtime_root: &Path,
126+
input_file: &Path,
127+
) -> Result<(String, Option<Vec<u8>>), String> {
128+
let canonical_root = runtime_root.canonicalize().map_err(|err| {
129+
format!(
130+
"failed to resolve runtime root {}: {err}",
131+
runtime_root.display()
132+
)
133+
})?;
134+
let canonical_input = input_file.canonicalize().map_err(|err| {
135+
format!(
136+
"failed to resolve analysis input {}: {err}",
137+
input_file.display()
138+
)
139+
})?;
140+
if let Ok(relative) = canonical_input.strip_prefix(&canonical_root) {
141+
let relative_text = relative.display().to_string();
142+
if !relative_text.is_empty() {
143+
return Ok((relative_text, None));
144+
}
145+
}
146+
147+
let input_bytes = fs::read(input_file).map_err(|err| {
148+
format!(
149+
"failed to read analysis input {}: {err}",
150+
input_file.display()
151+
)
152+
})?;
153+
Ok(("/input/genotypes".to_owned(), Some(input_bytes)))
154+
}
155+
156+
struct AnalysisVirtualTextFiles {
157+
text_files: BTreeMap<String, String>,
158+
asset_paths: BTreeMap<String, String>,
159+
}
160+
161+
fn collect_analysis_virtual_text_files(
162+
input: &BioscriptAnalysisScriptInput<'_>,
163+
script_virtual_path: &str,
164+
script_text: String,
165+
manifest_virtual_path: &str,
166+
virtual_observations_file: &str,
167+
observations_text: String,
168+
) -> Result<AnalysisVirtualTextFiles, String> {
169+
let mut virtual_text_files = BTreeMap::new();
170+
virtual_text_files.insert(script_virtual_path.to_owned(), script_text);
171+
virtual_text_files.insert(
172+
manifest_virtual_path.to_owned(),
173+
fs::read_to_string(input.manifest_path).map_err(|err| {
174+
format!(
175+
"failed to read analysis manifest {}: {err}",
176+
input.manifest_path.display()
177+
)
178+
})?,
179+
);
180+
virtual_text_files.insert(virtual_observations_file.to_owned(), observations_text);
181+
let mut asset_paths = BTreeMap::new();
182+
for asset in &input.interpretation.assets {
183+
let asset_path =
184+
resolve_manifest_path(input.runtime_root, input.manifest_path, &asset.path)?;
185+
let virtual_asset_path = virtual_pipeline_path(&asset_path, &asset.path);
186+
let text = fs::read_to_string(&asset_path).map_err(|err| {
187+
format!(
188+
"failed to read analysis asset {}: {err}",
189+
asset_path.display()
190+
)
191+
})?;
192+
virtual_text_files.insert(virtual_asset_path.clone(), text);
193+
asset_paths.insert(asset.id.clone(), virtual_asset_path);
194+
}
195+
Ok(AnalysisVirtualTextFiles {
196+
text_files: virtual_text_files,
197+
asset_paths,
198+
})
199+
}
200+
201+
fn persist_virtual_output_files(
202+
written: &BTreeMap<String, String>,
203+
primary_virtual_output_file: &str,
204+
primary_output_file: &Path,
205+
) -> Result<(), String> {
206+
let Some(output_dir) = primary_output_file.parent() else {
207+
return Ok(());
208+
};
209+
for (virtual_path, text) in written {
210+
if virtual_path == primary_virtual_output_file {
211+
continue;
212+
}
213+
let Some(relative) = virtual_path.strip_prefix("/output/") else {
214+
continue;
215+
};
216+
let relative_path = Path::new(relative);
217+
if relative_path
218+
.components()
219+
.any(|component| !matches!(component, std::path::Component::Normal(_)))
220+
{
221+
return Err(format!("analysis wrote invalid output path {virtual_path}"));
222+
}
223+
let output_path = output_dir.join(relative_path);
224+
if let Some(parent) = output_path.parent() {
225+
fs::create_dir_all(parent).map_err(|err| {
226+
format!("failed to create output dir {}: {err}", parent.display())
227+
})?;
228+
}
229+
fs::write(&output_path, text).map_err(|err| {
230+
format!(
231+
"failed to write analysis output {}: {err}",
232+
output_path.display()
233+
)
234+
})?;
235+
}
236+
Ok(())
237+
}
238+
239+
fn virtual_pipeline_path(path: &Path, fallback: &str) -> String {
240+
let name = path
241+
.file_name()
242+
.and_then(|value| value.to_str())
243+
.unwrap_or(fallback);
244+
format!("/input/pipeline/{name}")
245+
}
246+
247+
fn analysis_context(
248+
participant_id: &str,
249+
input_file: &str,
250+
script_path: &str,
251+
manifest_path: &str,
252+
asset_paths: &BTreeMap<String, String>,
253+
observations_file: &str,
254+
output_file: &str,
255+
) -> BTreeMap<String, monty::MontyObject> {
256+
BTreeMap::from([
257+
(
258+
"participant_id".to_owned(),
259+
monty::MontyObject::String(participant_id.to_owned()),
260+
),
261+
(
262+
"input_files".to_owned(),
263+
monty_string_dict(&BTreeMap::from([(
264+
"genotypes".to_owned(),
265+
input_file.to_owned(),
266+
)])),
267+
),
268+
(
269+
"pipeline_files".to_owned(),
270+
monty_string_dict(&BTreeMap::from([
271+
("manifest".to_owned(), manifest_path.to_owned()),
272+
("analysis".to_owned(), script_path.to_owned()),
273+
])),
274+
),
275+
("assets".to_owned(), monty_string_dict(asset_paths)),
276+
(
277+
"observations_file".to_owned(),
278+
monty::MontyObject::String(observations_file.to_owned()),
279+
),
280+
(
281+
"output_file".to_owned(),
282+
monty::MontyObject::String(output_file.to_owned()),
283+
),
284+
])
285+
}
286+
287+
fn parse_analysis_output(
288+
path: &Path,
289+
format: &str,
290+
) -> Result<(Vec<serde_json::Value>, Vec<String>), String> {
291+
let text = fs::read_to_string(path)
292+
.map_err(|err| format!("failed to read analysis output {}: {err}", path.display()))?;
293+
bioscript_reporting::parse_analysis_output_text(&text, format)
294+
.map_err(|err| format!("failed to parse analysis output {}: {err}", path.display()))
295+
}

0 commit comments

Comments
 (0)