use anyhow::{Context, Result};
use base64::Engine;
use base64::engine::general_purpose::STANDARD as BASE64;
use reqwest::Client;
use crate::client;
async fn fetch_ipynb(http: &Client, ws_id: &str, item_id: &str) -> Result<serde_json::Value> {
let def = client::get_definition(http, ws_id, item_id, "ipynb").await?;
let parts = def.definition.context("No definition returned")?.parts;
let part = parts
.iter()
.find(|p| p.path.contains("notebook-content") || p.path.ends_with(".ipynb"))
.or_else(|| parts.first())
.context("No parts in definition")?;
let decoded = BASE64
.decode(&part.payload)
.context("Failed to decode base64 payload")?;
let ipynb: serde_json::Value =
serde_json::from_slice(&decoded).context("Failed to parse ipynb JSON")?;
Ok(ipynb)
}
fn detect_language_group(ipynb: &serde_json::Value) -> String {
ipynb
.pointer("/metadata/microsoft/language_group")
.and_then(|v| v.as_str())
.unwrap_or("jupyter_python")
.to_string()
}
pub async fn run_cells_list(http: &Client, reference: &str) -> Result<()> {
let (ws_name, nb_name) = client::parse_ref(reference)?;
let ws_id = client::resolve_workspace(http, ws_name).await?;
let nb = client::resolve_item(http, &ws_id, nb_name, "Notebook").await?;
let ipynb = fetch_ipynb(http, &ws_id, &nb.id).await?;
let cells = ipynb
.get("cells")
.and_then(|c| c.as_array())
.context("No cells array in notebook")?;
if cells.is_empty() {
println!(" No cells in '{}'", nb.display_name);
return Ok(());
}
println!(" {:<5} {:<10} {}", "Index", "Type", "First line");
println!(" {:<5} {:<10} {}", "-----", "----------", "----------");
for (i, cell) in cells.iter().enumerate() {
let cell_type = cell
.get("cell_type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let first_line = cell_source_string(cell);
let first_line = first_line.lines().next().unwrap_or("");
let truncated = if first_line.len() > 80 {
format!("{}...", &first_line[..77])
} else {
first_line.to_string()
};
println!(" {:<5} {:<10} {}", i, cell_type, truncated);
}
println!("\n {} cell(s)", cells.len());
Ok(())
}
pub async fn run_cell_view(http: &Client, reference: &str, index: usize) -> Result<()> {
let (ws_name, nb_name) = client::parse_ref(reference)?;
let ws_id = client::resolve_workspace(http, ws_name).await?;
let nb = client::resolve_item(http, &ws_id, nb_name, "Notebook").await?;
let ipynb = fetch_ipynb(http, &ws_id, &nb.id).await?;
let cells = ipynb
.get("cells")
.and_then(|c| c.as_array())
.context("No cells array in notebook")?;
let cell = cells
.get(index)
.context(format!("Cell index {} out of range (0..{})", index, cells.len()))?;
let cell_type = cell
.get("cell_type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
let source = cell_source_string(cell);
println!(" Cell {} ({})", index, cell_type);
println!(" {}", "-".repeat(40));
println!("{}", source);
Ok(())
}
pub async fn run_cell_add(
http: &Client,
reference: &str,
code: &str,
markdown: bool,
at: Option<usize>,
) -> Result<()> {
let (ws_name, nb_name) = client::parse_ref(reference)?;
let ws_id = client::resolve_workspace(http, ws_name).await?;
let nb = client::resolve_item(http, &ws_id, nb_name, "Notebook").await?;
let mut ipynb = fetch_ipynb(http, &ws_id, &nb.id).await?;
let language_group = detect_language_group(&ipynb);
let cell_type = if markdown { "markdown" } else { "code" };
let source_lines: Vec<serde_json::Value> = code
.lines()
.enumerate()
.map(|(i, line)| {
let total = code.lines().count();
if i < total - 1 {
serde_json::Value::String(format!("{}\n", line))
} else {
serde_json::Value::String(line.to_string())
}
})
.collect();
let mut new_cell = serde_json::json!({
"cell_type": cell_type,
"metadata": {
"microsoft": {
"language": "python",
"language_group": language_group
}
},
"source": source_lines
});
if cell_type == "code" {
new_cell["outputs"] = serde_json::json!([]);
new_cell["execution_count"] = serde_json::Value::Null;
}
let cells = ipynb
.get_mut("cells")
.and_then(|c| c.as_array_mut())
.context("No cells array in notebook")?;
match at {
Some(pos) => {
if pos > cells.len() {
anyhow::bail!(
"Insert position {} out of range (0..{})",
pos,
cells.len()
);
}
cells.insert(pos, new_cell);
println!(" Inserted {} cell at index {}", cell_type, pos);
}
None => {
let idx = cells.len();
cells.push(new_cell);
println!(" Appended {} cell at index {}", cell_type, idx);
}
}
client::update_definition(http, &ws_id, &nb.id, &ipynb).await?;
println!(" Definition updated.");
Ok(())
}
pub async fn run_cell_edit(
http: &Client,
reference: &str,
index: usize,
code: &str,
) -> Result<()> {
let (ws_name, nb_name) = client::parse_ref(reference)?;
let ws_id = client::resolve_workspace(http, ws_name).await?;
let nb = client::resolve_item(http, &ws_id, nb_name, "Notebook").await?;
let mut ipynb = fetch_ipynb(http, &ws_id, &nb.id).await?;
let cells = ipynb
.get_mut("cells")
.and_then(|c| c.as_array_mut())
.context("No cells array in notebook")?;
let cells_len = cells.len();
let cell = cells
.get_mut(index)
.context(format!("Cell index {} out of range (0..{})", index, cells_len))?;
let source_lines: Vec<serde_json::Value> = code
.lines()
.enumerate()
.map(|(i, line)| {
let total = code.lines().count();
if i < total - 1 {
serde_json::Value::String(format!("{}\n", line))
} else {
serde_json::Value::String(line.to_string())
}
})
.collect();
cell["source"] = serde_json::Value::Array(source_lines);
let cell_type = cell
.get("cell_type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
println!(" Updated {} cell at index {}", cell_type, index);
client::update_definition(http, &ws_id, &nb.id, &ipynb).await?;
println!(" Definition updated.");
Ok(())
}
pub async fn run_cell_rm(http: &Client, reference: &str, index: usize) -> Result<()> {
let (ws_name, nb_name) = client::parse_ref(reference)?;
let ws_id = client::resolve_workspace(http, ws_name).await?;
let nb = client::resolve_item(http, &ws_id, nb_name, "Notebook").await?;
let mut ipynb = fetch_ipynb(http, &ws_id, &nb.id).await?;
let cells = ipynb
.get_mut("cells")
.and_then(|c| c.as_array_mut())
.context("No cells array in notebook")?;
if index >= cells.len() {
anyhow::bail!(
"Cell index {} out of range (0..{})",
index,
cells.len()
);
}
let removed = cells.remove(index);
let cell_type = removed
.get("cell_type")
.and_then(|v| v.as_str())
.unwrap_or("unknown");
println!(" Removed {} cell at index {}", cell_type, index);
client::update_definition(http, &ws_id, &nb.id, &ipynb).await?;
println!(" Definition updated.");
Ok(())
}
pub fn cell_source_string(cell: &serde_json::Value) -> String {
match cell.get("source") {
Some(serde_json::Value::Array(arr)) => arr
.iter()
.filter_map(|v| v.as_str())
.collect::<Vec<_>>()
.join(""),
Some(serde_json::Value::String(s)) => s.clone(),
_ => String::new(),
}
}