Skip to content

Commit d3e5d54

Browse files
author
Dexter Duckworth
committed
Added basic CLI tools.
- Created `ella-cli` crate. - Implemented `serve` command to run a standalone server. - Implemented `connect` and `open` commands to start a very basic REPL. - Added pretty printing for dataframes.
1 parent afb41a9 commit d3e5d54

File tree

20 files changed

+4129
-22
lines changed

20 files changed

+4129
-22
lines changed

ella-cli/Cargo.lock

Lines changed: 3662 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ella-cli/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "ella-cli"
3+
version = "0.1.1"
4+
edition = "2021"
5+
authors = ["Dexter Duckworth <[email protected]>"]
6+
license = "MIT OR Apache-2.0"
7+
repository = "https://github.com/BlackrockNeurotech/ella"
8+
description = "CLI tools for working with ella datastores."
9+
10+
[[bin]]
11+
name = "ella"
12+
path = "src/main.rs"
13+
14+
[dependencies]
15+
ella = { path = "../ella", version = "0.1.1" }
16+
17+
anyhow = "1.0.72"
18+
tracing = "0.1.37"
19+
tracing-subscriber = "0.3.17"
20+
futures = "0.3.28"
21+
tokio = { version = "1.27.0", features = ["full"] }
22+
mimalloc = { version = "0.1.37", default-features = false }
23+
clap = { version = "4.3.19", features = ["derive"] }
24+
dialoguer = { version = "0.10.4", features = ["history"] }
25+
shlex = "1.1.0"
26+
27+
[package.metadata.docs.rs]
28+
rustc-args = ["--cfg", "uuid_unstable"]
29+
rustdoc-args = ["--cfg", "uuid_unstable"]

ella-cli/README.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# ella-cli
2+
3+
## Installation
4+
5+
### Cargo
6+
7+
```shell
8+
cargo install --locked ella-cli
9+
```
10+
11+
## Usage
12+
13+
*[See `ella` CLI reference](docs/reference.md)*

ella-cli/docs/reference.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
# `ella` Reference
2+
3+
```console
4+
$ ella -h
5+
CLI tools for working with ella datastores.
6+
7+
Usage: ella [OPTIONS] <COMMAND>
8+
9+
Commands:
10+
serve Open a datastore as a standalone server
11+
connect Start an interactive session by connecting to an ella API server
12+
open Start an interactive session by opening a local datastore
13+
help Print this message or the help of the given subcommand(s)
14+
15+
Options:
16+
-v, --verbose...
17+
-q, --quiet...
18+
-h, --help Print help
19+
-V, --version Print version
20+
```

ella-cli/src/connect.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
/// Start an interactive session by connecting to an ella API server
2+
#[derive(Debug, clap::Args)]
3+
pub struct Args {
4+
/// Address of the API server
5+
#[arg(default_value = "http://localhost:50052")]
6+
addr: String,
7+
}
8+
9+
pub async fn run(args: Args, ctx: crate::Context) -> anyhow::Result<()> {
10+
let rt = ella::connect(args.addr).await?;
11+
crate::interactive::interactive(rt, 100, ctx).await
12+
}

ella-cli/src/interactive.rs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
use clap::{CommandFactory, Parser};
2+
use dialoguer::{console::style, History, Input};
3+
use ella::Ella;
4+
use std::collections::VecDeque;
5+
use tracing::metadata::LevelFilter;
6+
7+
#[derive(Debug, clap::Parser)]
8+
#[command(
9+
multicall = true,
10+
help_template = "{all-args}",
11+
disable_help_subcommand = true,
12+
disable_help_flag = true,
13+
disable_version_flag = true
14+
)]
15+
pub struct Args {
16+
#[command(subcommand)]
17+
action: Action,
18+
}
19+
20+
#[derive(Debug, clap::Subcommand)]
21+
enum Action {
22+
/// Quit the session
23+
#[command(visible_alias = "\\q")]
24+
Quit,
25+
/// Display help
26+
#[command(visible_alias = "\\h")]
27+
Help,
28+
#[command(external_subcommand)]
29+
Sql(Vec<String>),
30+
}
31+
32+
pub async fn interactive(rt: Ella, history: usize, ctx: crate::Context) -> anyhow::Result<()> {
33+
crate::init_logging(ctx.verbosity.log_level(LevelFilter::WARN));
34+
35+
let mut history = CmdHistory::new(history);
36+
loop {
37+
let cmd = Input::<String>::new()
38+
.with_prompt(rt.default_catalog().to_string())
39+
.history_with(&mut history)
40+
.allow_empty(true)
41+
.interact_text()?;
42+
43+
match shlex::split(&cmd) {
44+
Some(args) => match Args::try_parse_from(args) {
45+
Ok(args) => match args.action {
46+
Action::Quit => break,
47+
Action::Help => Args::command().print_help().unwrap(),
48+
Action::Sql(sql) => match rt.query(sql.join(" ")).await {
49+
Ok(plan) => match plan.execute().await {
50+
Ok(df) => {
51+
println!("{}", df.pretty_print())
52+
}
53+
Err(error) => {
54+
println!("{}: {}", style("error").red(), error);
55+
}
56+
},
57+
Err(error) => println!("{}: {}", style("error").red(), error),
58+
},
59+
},
60+
Err(_) => Args::command().print_help().unwrap(),
61+
},
62+
None => Args::command().print_help().unwrap(),
63+
}
64+
}
65+
Ok(())
66+
}
67+
68+
#[derive(Debug)]
69+
struct CmdHistory {
70+
capacity: usize,
71+
history: VecDeque<String>,
72+
}
73+
74+
impl CmdHistory {
75+
fn new(capacity: usize) -> Self {
76+
Self {
77+
capacity,
78+
history: VecDeque::with_capacity(capacity),
79+
}
80+
}
81+
}
82+
83+
impl<T: ToString> History<T> for CmdHistory {
84+
fn read(&self, pos: usize) -> Option<String> {
85+
self.history.get(pos).cloned()
86+
}
87+
88+
fn write(&mut self, val: &T) {
89+
if self.history.len() == self.capacity {
90+
self.history.pop_back();
91+
}
92+
self.history.push_front(val.to_string());
93+
}
94+
}

ella-cli/src/main.rs

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
mod connect;
2+
mod interactive;
3+
mod open;
4+
mod serve;
5+
6+
use clap::Parser;
7+
use tracing::{metadata::LevelFilter, Level};
8+
use tracing_subscriber::{
9+
prelude::__tracing_subscriber_SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer,
10+
};
11+
12+
#[derive(Debug, Parser)]
13+
#[command(author, version, about)]
14+
struct Args {
15+
#[command(subcommand)]
16+
action: Action,
17+
#[command(flatten)]
18+
verbosity: Verbosity,
19+
}
20+
21+
#[derive(Debug, clap::Subcommand)]
22+
enum Action {
23+
Serve(serve::Args),
24+
Connect(connect::Args),
25+
Open(open::Args),
26+
}
27+
28+
#[tokio::main]
29+
async fn main() -> anyhow::Result<()> {
30+
let args = Args::parse();
31+
let ctx = Context {
32+
verbosity: args.verbosity,
33+
};
34+
35+
use Action::*;
36+
match args.action {
37+
Serve(args) => serve::run(args, ctx).await?,
38+
Connect(args) => connect::run(args, ctx).await?,
39+
Open(args) => open::run(args, ctx).await?,
40+
}
41+
Ok(())
42+
}
43+
44+
#[derive(Debug)]
45+
pub struct Context {
46+
pub verbosity: Verbosity,
47+
}
48+
49+
#[derive(Debug, Clone, clap::Args)]
50+
pub struct Verbosity {
51+
#[arg(short, long, action = clap::ArgAction::Count, global = true, conflicts_with = "quiet")]
52+
verbose: u8,
53+
#[arg(short, long, action = clap::ArgAction::Count, global = true, conflicts_with = "verbose")]
54+
quiet: u8,
55+
}
56+
57+
impl Verbosity {
58+
pub fn log_level(&self, level: LevelFilter) -> LevelFilter {
59+
let mut level: Option<Level> = level.into();
60+
for _ in 0..self.verbose {
61+
level = match level {
62+
None => Some(Level::ERROR),
63+
Some(l) => Some(match l {
64+
Level::TRACE => Level::TRACE,
65+
Level::DEBUG => Level::TRACE,
66+
Level::INFO => Level::DEBUG,
67+
Level::WARN => Level::INFO,
68+
Level::ERROR => Level::WARN,
69+
}),
70+
}
71+
}
72+
73+
for _ in 0..self.quiet {
74+
level = match level {
75+
None | Some(Level::ERROR) => None,
76+
Some(l) => Some(match l {
77+
Level::TRACE => Level::DEBUG,
78+
Level::DEBUG => Level::INFO,
79+
Level::INFO => Level::WARN,
80+
Level::WARN => Level::ERROR,
81+
_ => unreachable!(),
82+
}),
83+
}
84+
}
85+
level.into()
86+
}
87+
}
88+
89+
fn init_logging(level: LevelFilter) {
90+
tracing_subscriber::registry()
91+
.with(
92+
tracing_subscriber::fmt::layer().with_filter(
93+
EnvFilter::builder()
94+
.with_default_directive(level.into())
95+
.with_env_var("ELLE_LOG")
96+
.from_env()
97+
.unwrap(),
98+
),
99+
)
100+
.init();
101+
}

ella-cli/src/open.rs

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
use ella::Path;
2+
3+
/// Start an interactive session by opening a local datastore
4+
#[derive(Debug, clap::Args)]
5+
pub struct Args {
6+
root: Path,
7+
#[arg(long)]
8+
create: bool,
9+
}
10+
11+
pub async fn run(args: Args, ctx: crate::Context) -> anyhow::Result<()> {
12+
let rt = if args.create {
13+
ella::open(args.root.to_string())
14+
.or_create_default()
15+
.await?
16+
} else {
17+
ella::open(args.root.to_string()).await?
18+
};
19+
crate::interactive::interactive(rt, 100, ctx).await
20+
}

ella-cli/src/serve.rs

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
use tracing::metadata::LevelFilter;
2+
3+
/// Open a datastore as a standalone server.
4+
///
5+
/// The datastore will be created if it doesn't already exist.
6+
#[derive(Debug, clap::Args)]
7+
pub struct Args {
8+
/// Path to the datastore root
9+
root: ella::Path,
10+
/// Address where the ella API will be served
11+
#[arg(short, long, default_value = "localhost:50052")]
12+
addr: String,
13+
/// Do not create the datastore if it doesn't already exist
14+
#[arg(long)]
15+
no_create: bool,
16+
}
17+
18+
pub async fn run(args: Args, ctx: crate::Context) -> anyhow::Result<()> {
19+
crate::init_logging(ctx.verbosity.log_level(LevelFilter::INFO));
20+
21+
tracing::info!("starting elle server");
22+
let rt = if args.no_create {
23+
ella::open(args.root.to_string())
24+
.and_serve(args.addr)?
25+
.await
26+
} else {
27+
ella::open(args.root.to_string())
28+
.or_create_default()
29+
.and_serve(args.addr)?
30+
.await
31+
}?;
32+
if let Err(error) = tokio::signal::ctrl_c().await {
33+
tracing::error!(?error, "failed to register signal listener");
34+
}
35+
36+
tracing::info!("shutting down server");
37+
rt.shutdown().await?;
38+
39+
Ok(())
40+
}

ella-common/src/tensor_value.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ pub trait TensorValue: Debug + Clone + PartialEq + PartialOrd + Send + Sync + 's
1414
/// Masked value type. For `Option<T>` this is `Option<T>`.
1515
/// For all other `T` this should be `Option<T>`.
1616
type Masked: TensorValue<Array = Self::Array>;
17-
/// Unmasked value type. For Option<T> shis is `T`. For all other `T` this should be `T`.
17+
/// Unmasked value type. For `Option<T>` this is `T`. For all other `T` this should be `T`.
1818
type Unmasked: TensorValue<Array = Self::Array>;
1919

2020
const TENSOR_TYPE: TensorType;
@@ -84,7 +84,7 @@ pub trait TensorValue: Debug + Clone + PartialEq + PartialOrd + Send + Sync + 's
8484
///
8585
/// Panics if `offset + length` > `array.len()`.
8686
fn slice(array: &Self::Array, offset: usize, length: usize) -> Self::Array;
87-
/// Constructs an array from [`ArrowData`].
87+
/// Constructs an array from [`ArrayData`].
8888
///
8989
/// Panics if `data` is not convertable to [`Self::Array`].
9090
fn from_array_data(data: ArrayData) -> Self::Array;

0 commit comments

Comments
 (0)