Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Install cli with pip #38

Merged
merged 4 commits into from
Oct 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/python.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ jobs:
run: uv run mypy . && uv run ruff check && uv run ruff format --check
- name: Test
run: uv run pytest
- name: CLI smoke test
run: uv run cql2 < ../fixtures/text/example01.txt
linux:
runs-on: ${{ matrix.platform.runner }}
strategy:
Expand Down
21 changes: 15 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,6 @@ rstest = "0.23"
[workspace]
default-members = [".", "cli"]
members = ["cli", "python"]

[workspace.dependencies]
clap = "4.5"
5 changes: 3 additions & 2 deletions cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name = "cql2-cli"
version = "0.1.0"
authors = ["David Bitner <[email protected]>"]
edition = "2021"
description = "Command line interface (CLI) for Common Query Language (CQL2)"
description = "Command line interface for Common Query Language (CQL2)"
readme = "README.md"
homepage = "https://github.com/developmentseed/cql2-rs"
repository = "https://github.com/developmentseed/cql2-rs"
Expand All @@ -12,8 +12,9 @@ keywords = ["cql2"]


[dependencies]
anyhow = "1.0"
clap = { workspace = true, features = ["derive"] }
cql2 = { path = "..", version = "0.1.0" }
clap = { version = "4.5", features = ["derive"] }
serde_json = "1.0"

[[bin]]
Expand Down
9 changes: 7 additions & 2 deletions cli/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,18 @@ A Command Line Interface (CLI) for [Common Query Language (CQL2)](https://www.og

## Installation

Install [Rust](https://rustup.rs/).
Then:
With cargo:

```shell
cargo install cql2-cli
```

Or from [PyPI](https://pypi.org/project/cql2/):

```shell
pip install cql2
```

## CLI

At its simplest, the CLI is a pass-through validator:
Expand Down
145 changes: 145 additions & 0 deletions cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
use anyhow::{anyhow, Result};
use clap::{ArgAction, Parser, ValueEnum};
use cql2::{Expr, Validator};
use std::io::Read;

/// The CQL2 command-line interface.
#[derive(Debug, Parser)]
#[command(version, about, long_about = None)]
pub struct Cli {
/// The input CQL2
///
/// If not provided, or `-`, the CQL2 will be read from standard input. The
/// type (json or text) will be auto-detected. To specify a format, use
/// --input-format.
input: Option<String>,

/// The input format.
///
/// If not provided, the format will be auto-detected from the input.
#[arg(short, long)]
input_format: Option<InputFormat>,

/// The output format.
///
/// If not provided, the format will be the same as the input.
#[arg(short, long)]
output_format: Option<OutputFormat>,

/// Validate the CQL2
#[arg(long, default_value_t = true, action = ArgAction::Set)]
validate: bool,

/// Verbosity.
///
/// Provide this argument several times to turn up the chatter.
#[arg(short, long, action = ArgAction::Count)]
verbose: u8,
}

/// The input CQL2 format.
#[derive(Debug, ValueEnum, Clone)]
pub enum InputFormat {
/// cql2-json
Json,

/// cql2-text
Text,
}

/// The output CQL2 format.
#[derive(Debug, ValueEnum, Clone)]
enum OutputFormat {
/// cql2-json, pretty-printed
JsonPretty,

/// cql2-json, compact
Json,

/// cql2-text
Text,

/// SQL
Sql,
}

impl Cli {
/// Runs the cli.
///
/// # Examples
///
/// ```
/// use cql2_cli::Cli;
/// use clap::Parser;
///
/// let cli = Cli::try_parse_from(&["cql2", "landsat:scene_id = 'LC82030282019133LGN00'"]).unwrap();
/// cli.run();
/// ```
pub fn run(self) {
if let Err(err) = self.run_inner() {
eprintln!("{}", err);
std::process::exit(1)
}
}

pub fn run_inner(self) -> Result<()> {
let input = self
.input
.and_then(|input| if input == "-" { None } else { Some(input) })
.map(Ok)
.unwrap_or_else(read_stdin)?;
let input_format = self.input_format.unwrap_or_else(|| {
if input.starts_with('{') {
InputFormat::Json
} else {
InputFormat::Text
}
});
let expr: Expr = match input_format {
InputFormat::Json => cql2::parse_json(&input)?,
InputFormat::Text => match cql2::parse_text(&input) {
Ok(expr) => expr,
Err(err) => {
return Err(anyhow!("[ERROR] Parsing error: {input}\n{err}"));
}
},
};
if self.validate {
let validator = Validator::new().unwrap();
let value = serde_json::to_value(&expr).unwrap();
if let Err(error) = validator.validate(&value) {
return Err(anyhow!(
"[ERROR] Invalid CQL2: {input}\n{}",
match self.verbose {
0 => "For more detailed validation information, use -v".to_string(),
1 => format!("For more detailed validation information, use -vv\n{error}"),
2 =>
format!("For more detailed validation information, use -vvv\n{error:#}"),
_ => {
let detailed_output = error.detailed_output();
format!("{detailed_output:#}")
}
}
));
}
}
let output_format = self.output_format.unwrap_or(match input_format {
InputFormat::Json => OutputFormat::Json,
InputFormat::Text => OutputFormat::Text,
});
match output_format {
OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr)?,
OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr)?,
OutputFormat::Text => print!("{}", expr.to_text()?),
OutputFormat::Sql => serde_json::to_writer_pretty(std::io::stdout(), &expr.to_sql()?)?,
}
println!();
Ok(())
}
}

fn read_stdin() -> Result<String> {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf)?;
Ok(buf)
}
121 changes: 3 additions & 118 deletions cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,121 +1,6 @@
use clap::{ArgAction, Parser, ValueEnum};
use cql2::{Expr, Validator};
use std::io::Read;

#[derive(Debug, Parser)]
struct Cli {
/// The input CQL2
///
/// If not provided, or `-`, the CQL2 will be read from standard input. The
/// type (json or text) will be auto-detected. To specify a format, use
/// --input-format.
input: Option<String>,

/// The input format.
///
/// If not provided, the format will be auto-detected from the input.
#[arg(short, long)]
input_format: Option<InputFormat>,

/// The output format.
///
/// If not provided, the format will be the same as the input.
#[arg(short, long)]
output_format: Option<OutputFormat>,

/// Validate the CQL2
#[arg(long, default_value_t = true, action = ArgAction::Set)]
validate: bool,

/// Verbosity.
///
/// Provide this argument several times to turn up the chatter.
#[arg(short, long, action = ArgAction::Count)]
verbose: u8,
}

#[derive(Debug, ValueEnum, Clone)]
enum InputFormat {
/// cql2-json
Json,

/// cql2-text
Text,
}

#[derive(Debug, ValueEnum, Clone)]
enum OutputFormat {
/// cql2-json, pretty-printed
JsonPretty,

/// cql2-json, compact
Json,

/// cql2-text
Text,

/// SQL
Sql,
}
use clap::Parser;
use cql2_cli::Cli;

fn main() {
let cli = Cli::parse();
let input = cli
.input
.and_then(|input| if input == "-" { None } else { Some(input) })
.unwrap_or_else(read_stdin);
let input_format = cli.input_format.unwrap_or_else(|| {
if input.starts_with('{') {
InputFormat::Json
} else {
InputFormat::Text
}
});
let expr: Expr = match input_format {
InputFormat::Json => cql2::parse_json(&input).unwrap(),
InputFormat::Text => match cql2::parse_text(&input) {
Ok(expr) => expr,
Err(err) => {
eprintln!("[ERROR] Parsing error: {input}");
eprintln!("{err}");
std::process::exit(1)
}
},
};
if cli.validate {
let validator = Validator::new().unwrap();
let value = serde_json::to_value(&expr).unwrap();
if let Err(error) = validator.validate(&value) {
eprintln!("[ERROR] Invalid CQL2: {input}");
match cli.verbose {
0 => eprintln!("For more detailed validation information, use -v"),
1 => eprintln!("For more detailed validation information, use -vv\n{error}"),
2 => eprintln!("For more detailed validation information, use -vvv\n{error:#}"),
_ => {
let detailed_output = error.detailed_output();
eprintln!("{detailed_output:#}");
}
}
std::process::exit(1)
}
}
let output_format = cli.output_format.unwrap_or(match input_format {
InputFormat::Json => OutputFormat::Json,
InputFormat::Text => OutputFormat::Text,
});
match output_format {
OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr).unwrap(),
OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr).unwrap(),
OutputFormat::Text => print!("{}", expr.to_text().unwrap()),
OutputFormat::Sql => {
serde_json::to_writer_pretty(std::io::stdout(), &expr.to_sql().unwrap()).unwrap()
}
}
println!()
}

fn read_stdin() -> String {
let mut buf = String::new();
std::io::stdin().read_to_string(&mut buf).unwrap();
buf
Cli::parse().run()
}
Loading
Loading