Split CLI into commands/ submodule
This commit is contained in:
parent
1badaeefb1
commit
69ec555a87
51
src/commands/init.rs
Normal file
51
src/commands/init.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use anyhow::{anyhow, bail, Context, Result};
|
||||||
|
use rusqlite::Connection;
|
||||||
|
use std::path::Path;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
fn init_schema(conn: &mut Connection, name: &str) -> Result<Uuid> {
|
||||||
|
let dataset_uuid =
|
||||||
|
nancy::db::init(conn, name).map_err(|e| anyhow!("failed to initialize schema: {}", e))?;
|
||||||
|
|
||||||
|
log::trace!("Init OK");
|
||||||
|
log::info!("Dataset UUID is {dataset_uuid}");
|
||||||
|
// Run an empty program so that the dataset log reflects when it was
|
||||||
|
// initialized
|
||||||
|
nancy::program::with_program(conn, "INIT", "Initialize dataset", |prog| {
|
||||||
|
let _ = prog.perform_task(&[], |task| {
|
||||||
|
log::debug!("INIT task UUID is {}", task.key);
|
||||||
|
Ok::<(), ()>(())
|
||||||
|
});
|
||||||
|
let okres: Result<()> = Ok(());
|
||||||
|
okres
|
||||||
|
})
|
||||||
|
.context("Could not run empty program during init_schema")??;
|
||||||
|
|
||||||
|
Ok(dataset_uuid)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run init subcommand and return the return code
|
||||||
|
pub fn init_cmd(name: &str, dataset_path: &Path) -> Result<Uuid> {
|
||||||
|
if !dataset_path.is_dir() {
|
||||||
|
bail!(
|
||||||
|
"Path {:?} does not point to an existing directory",
|
||||||
|
dataset_path
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let dbpath = &dataset_path.join("nancy.db");
|
||||||
|
if dbpath.exists() {
|
||||||
|
bail!(
|
||||||
|
"Database {:?} exists, indicating this dataset is already \
|
||||||
|
initialized. Refusing to overwrite.",
|
||||||
|
dbpath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
log::info!("Initializing new database at {:?}", dbpath);
|
||||||
|
let mut conn = Connection::open(dbpath)
|
||||||
|
.with_context(|| format!("Could not open new SQLite database at {dbpath:?}"))?;
|
||||||
|
conn.pragma_update(None, "foreign_keys", &"ON")
|
||||||
|
.context("Could not set foreign_keys pragma")?;
|
||||||
|
let u = init_schema(&mut conn, name)?;
|
||||||
|
|
||||||
|
Ok(u)
|
||||||
|
}
|
||||||
51
src/commands/mod.rs
Normal file
51
src/commands/mod.rs
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use clap::Subcommand;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub mod init;
|
||||||
|
pub mod record;
|
||||||
|
pub mod status;
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
pub enum Command {
|
||||||
|
/// Initialize a new dataset
|
||||||
|
#[command()]
|
||||||
|
Init {
|
||||||
|
/// A short descriptive name for the dataset
|
||||||
|
#[arg(short, long)]
|
||||||
|
name: String,
|
||||||
|
/// The top level directory of the dataset (must be a directory)
|
||||||
|
#[arg(default_value = ".")]
|
||||||
|
dataset_path: PathBuf,
|
||||||
|
},
|
||||||
|
/// Record changes to files/directories within a dataset (or create a new dataset)
|
||||||
|
#[command()]
|
||||||
|
Record {
|
||||||
|
/// A short descriptive message for this recording, i.e. "Re-run with lr=1e-3"
|
||||||
|
#[arg(short, long)]
|
||||||
|
message: String,
|
||||||
|
/// Paths to record. Defaults to current dataset or current directory if not in a dataset.
|
||||||
|
#[arg()]
|
||||||
|
record_paths: Vec<PathBuf>,
|
||||||
|
},
|
||||||
|
/// Check for changes in dataset and print basic statistics
|
||||||
|
Status {
|
||||||
|
/// Paths to display current status. Defaults to dataset containing current directory.
|
||||||
|
#[arg()]
|
||||||
|
status_paths: Vec<PathBuf>,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(command: &Command) -> Result<()> {
|
||||||
|
match &command {
|
||||||
|
Command::Init { name, dataset_path } => {
|
||||||
|
init::init_cmd(name, dataset_path)?;
|
||||||
|
}
|
||||||
|
Command::Record {
|
||||||
|
message,
|
||||||
|
record_paths,
|
||||||
|
} => record::record_cmd(message, record_paths)?,
|
||||||
|
Command::Status { status_paths } => status::status_cmd(status_paths)?,
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
41
src/commands/record.rs
Normal file
41
src/commands/record.rs
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
use anyhow::{anyhow, Context, Result};
|
||||||
|
use rusqlite::{Connection, OpenFlags};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use nancy::{fs, program};
|
||||||
|
|
||||||
|
pub fn record_cmd(message: &str, record_paths: &[PathBuf]) -> Result<()> {
|
||||||
|
// If no paths are given, use ["."] for the following steps.
|
||||||
|
|
||||||
|
// Determine dataset dir (ds_dir)
|
||||||
|
let dataset_path = nancy::db::find_dataset_dir(record_paths)
|
||||||
|
.with_context(|| "Could not determine dataset directory")?;
|
||||||
|
log::info!("Found existing dataset at path: {:?}", dataset_path);
|
||||||
|
let dbpath = &dataset_path.join("nancy.db");
|
||||||
|
|
||||||
|
// open with flags to prevent creating when we believe the db exists
|
||||||
|
let mut conn = Connection::open_with_flags(
|
||||||
|
dbpath,
|
||||||
|
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
||||||
|
)
|
||||||
|
.context("Could not open existing SQLite database at {dbpath:?}: {e:?}")?;
|
||||||
|
|
||||||
|
conn.pragma_update(None, "foreign_keys", &"ON")?;
|
||||||
|
|
||||||
|
nancy::db::ensure_schema(&mut conn)?;
|
||||||
|
|
||||||
|
// Note that recording may fail, in which case we should roll back only this program but keep
|
||||||
|
// the dataset initialized.
|
||||||
|
program::with_program(&mut conn, "RECORD", message, |prog| {
|
||||||
|
prog.perform_task(&[], |task| {
|
||||||
|
fs::record(
|
||||||
|
prog.transaction,
|
||||||
|
record_paths,
|
||||||
|
&dataset_path,
|
||||||
|
message,
|
||||||
|
task.key,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})??
|
||||||
|
.map_err(|e| anyhow!("Record program failed: {}", e))
|
||||||
|
}
|
||||||
25
src/commands/status.rs
Normal file
25
src/commands/status.rs
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
use anyhow::{Context, Result};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
pub fn status_cmd(status_paths: &[PathBuf]) -> Result<()> {
|
||||||
|
let status_paths = if status_paths.is_empty() {
|
||||||
|
vec![PathBuf::from(".").canonicalize()?]
|
||||||
|
} else {
|
||||||
|
status_paths.to_vec()
|
||||||
|
};
|
||||||
|
|
||||||
|
// Determine dataset dir (ds_dir)
|
||||||
|
let dataset_path = nancy::db::find_dataset_dir(&status_paths).with_context(|| {
|
||||||
|
format!(
|
||||||
|
"Could not find nancy.db in any directory containing given paths: {:?}",
|
||||||
|
status_paths
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
log::info!("Found existing dataset at path: {:?}", dataset_path);
|
||||||
|
let dbpath = &dataset_path.join("nancy.db");
|
||||||
|
|
||||||
|
println!("Computing status for paths: {:?}", status_paths);
|
||||||
|
|
||||||
|
log::error!("status_cmd not yet implemented");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
137
src/main.rs
137
src/main.rs
@ -1,143 +1,18 @@
|
|||||||
use anyhow::{anyhow, bail, Context, Result};
|
use anyhow::Result;
|
||||||
use clap::{Parser, Subcommand};
|
use clap::Parser;
|
||||||
use rusqlite::{Connection, OpenFlags};
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
mod commands;
|
||||||
|
|
||||||
use nancy::{fs, program};
|
/// Composable provenance tracking for scientific data analysis
|
||||||
|
|
||||||
// Composable provenance tracking for scientific data analysis
|
|
||||||
#[derive(Parser)]
|
#[derive(Parser)]
|
||||||
#[command(author, version, about, long_about = None, arg_required_else_help=true)]
|
#[command(author, version, about, long_about = None, arg_required_else_help=true)]
|
||||||
struct Cli {
|
struct Cli {
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
command: Command,
|
command: commands::Command,
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Subcommand)]
|
|
||||||
enum Command {
|
|
||||||
/// Initialize a new dataset
|
|
||||||
#[command()]
|
|
||||||
Init {
|
|
||||||
/// A short descriptive name for the dataset
|
|
||||||
#[arg(short, long)]
|
|
||||||
name: String,
|
|
||||||
/// The top level directory of the dataset (must be a directory)
|
|
||||||
#[arg(default_value = ".")]
|
|
||||||
dataset_path: PathBuf,
|
|
||||||
},
|
|
||||||
/// Record changes to files/directories within a dataset (or create a new dataset)
|
|
||||||
#[command()]
|
|
||||||
Record {
|
|
||||||
/// A short descriptive message for this recording, i.e. "Re-run with lr=1e-3"
|
|
||||||
#[arg(short, long)]
|
|
||||||
message: String,
|
|
||||||
#[arg()]
|
|
||||||
record_paths: Vec<PathBuf>,
|
|
||||||
},
|
|
||||||
/// Check for changes in dataset and print basic statistics
|
|
||||||
Status {},
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_schema(conn: &mut Connection, name: &str) -> Result<Uuid> {
|
|
||||||
let dataset_uuid =
|
|
||||||
nancy::db::init(conn, name).map_err(|e| anyhow!("failed to initialize schema: {}", e))?;
|
|
||||||
|
|
||||||
log::trace!("Init OK");
|
|
||||||
log::info!("Dataset UUID is {dataset_uuid}");
|
|
||||||
// Run an empty program so that the dataset log reflects when it was
|
|
||||||
// initialized
|
|
||||||
nancy::program::with_program(conn, "INIT", "Initialize dataset", |prog| {
|
|
||||||
let _ = prog.perform_task(&[], |task| {
|
|
||||||
log::debug!("INIT task UUID is {}", task.key);
|
|
||||||
Ok::<(), ()>(())
|
|
||||||
});
|
|
||||||
let okres: Result<()> = Ok(());
|
|
||||||
okres
|
|
||||||
})
|
|
||||||
.context("Could not run empty program during init_schema")??;
|
|
||||||
|
|
||||||
Ok(dataset_uuid)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run init subcommand and return the return code
|
|
||||||
fn init_cmd(name: &str, dataset_path: &Path) -> Result<Uuid> {
|
|
||||||
if !dataset_path.is_dir() {
|
|
||||||
bail!(
|
|
||||||
"Path {:?} does not point to an existing directory",
|
|
||||||
dataset_path
|
|
||||||
);
|
|
||||||
}
|
|
||||||
let dbpath = &dataset_path.join("nancy.db");
|
|
||||||
if dbpath.exists() {
|
|
||||||
bail!(
|
|
||||||
"Database {:?} exists, indicating this dataset is already \
|
|
||||||
initialized. Refusing to overwrite.",
|
|
||||||
dbpath
|
|
||||||
);
|
|
||||||
}
|
|
||||||
log::info!("Initializing new database at {:?}", dbpath);
|
|
||||||
let mut conn = Connection::open(dbpath)
|
|
||||||
.with_context(|| format!("Could not open new SQLite database at {dbpath:?}"))?;
|
|
||||||
conn.pragma_update(None, "foreign_keys", &"ON")
|
|
||||||
.context("Could not set foreign_keys pragma")?;
|
|
||||||
let u = init_schema(&mut conn, name)?;
|
|
||||||
|
|
||||||
Ok(u)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn record_cmd(message: &str, record_paths: &Vec<PathBuf>) -> Result<()> {
|
|
||||||
// If no paths are given, use ["."] for the following steps.
|
|
||||||
|
|
||||||
// Determine dataset dir (ds_dir)
|
|
||||||
let dataset_path = nancy::db::find_dataset_dir(record_paths)
|
|
||||||
.with_context(|| "Could not determine dataset directory")?;
|
|
||||||
log::info!("Found existing dataset at path: {:?}", dataset_path);
|
|
||||||
let dbpath = &dataset_path.join("nancy.db");
|
|
||||||
|
|
||||||
// open with flags to prevent creating when we believe the db exists
|
|
||||||
let mut conn = Connection::open_with_flags(
|
|
||||||
dbpath,
|
|
||||||
OpenFlags::SQLITE_OPEN_READ_WRITE | OpenFlags::SQLITE_OPEN_NO_MUTEX,
|
|
||||||
)
|
|
||||||
.context("Could not open existing SQLite database at {dbpath:?}: {e:?}")?;
|
|
||||||
|
|
||||||
conn.pragma_update(None, "foreign_keys", &"ON")?;
|
|
||||||
|
|
||||||
nancy::db::ensure_schema(&mut conn)?;
|
|
||||||
|
|
||||||
// Note that recording may fail, in which case we should roll back only this program but keep
|
|
||||||
// the dataset initialized.
|
|
||||||
program::with_program(&mut conn, "RECORD", message, |prog| {
|
|
||||||
prog.perform_task(&[], |task| {
|
|
||||||
fs::record(
|
|
||||||
prog.transaction,
|
|
||||||
record_paths.as_slice(),
|
|
||||||
&dataset_path,
|
|
||||||
message,
|
|
||||||
task.key,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
})??
|
|
||||||
.map_err(|e| anyhow!("Record program failed: {}", e))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<()> {
|
fn main() -> Result<()> {
|
||||||
env_logger::init();
|
env_logger::init();
|
||||||
let args = Cli::parse();
|
let args = Cli::parse();
|
||||||
|
commands::run(&args.command)
|
||||||
match &args.command {
|
|
||||||
Command::Init { name, dataset_path } => {
|
|
||||||
init_cmd(name, dataset_path)?;
|
|
||||||
}
|
|
||||||
Command::Record {
|
|
||||||
message,
|
|
||||||
record_paths,
|
|
||||||
} => record_cmd(message, record_paths)?,
|
|
||||||
Command::Status {} => {
|
|
||||||
println!("status not yet implemented");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Ok(())
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user