Skip to content

Commit 1d67fe2

Browse files
authored
Install cli with pip (#38)
* feat: add docs * fix(ci): use --workspace, not --all * feat: install cli via Pyton package
1 parent 66aef2c commit 1d67fe2

File tree

13 files changed

+219
-139
lines changed

13 files changed

+219
-139
lines changed

.github/workflows/python.yml

+2
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ jobs:
4040
run: uv run mypy . && uv run ruff check && uv run ruff format --check
4141
- name: Test
4242
run: uv run pytest
43+
- name: CLI smoke test
44+
run: uv run cql2 < ../fixtures/text/example01.txt
4345
linux:
4446
runs-on: ${{ matrix.platform.runner }}
4547
strategy:

Cargo.lock

+15-6
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -31,3 +31,6 @@ rstest = "0.23"
3131
[workspace]
3232
default-members = [".", "cli"]
3333
members = ["cli", "python"]
34+
35+
[workspace.dependencies]
36+
clap = "4.5"

cli/Cargo.toml

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ name = "cql2-cli"
33
version = "0.1.0"
44
authors = ["David Bitner <[email protected]>"]
55
edition = "2021"
6-
description = "Command line interface (CLI) for Common Query Language (CQL2)"
6+
description = "Command line interface for Common Query Language (CQL2)"
77
readme = "README.md"
88
homepage = "https://github.com/developmentseed/cql2-rs"
99
repository = "https://github.com/developmentseed/cql2-rs"
@@ -12,8 +12,9 @@ keywords = ["cql2"]
1212

1313

1414
[dependencies]
15+
anyhow = "1.0"
16+
clap = { workspace = true, features = ["derive"] }
1517
cql2 = { path = "..", version = "0.1.0" }
16-
clap = { version = "4.5", features = ["derive"] }
1718
serde_json = "1.0"
1819

1920
[[bin]]

cli/README.md

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,18 @@ A Command Line Interface (CLI) for [Common Query Language (CQL2)](https://www.og
44

55
## Installation
66

7-
Install [Rust](https://rustup.rs/).
8-
Then:
7+
With cargo:
98

109
```shell
1110
cargo install cql2-cli
1211
```
1312

13+
Or from [PyPI](https://pypi.org/project/cql2/):
14+
15+
```shell
16+
pip install cql2
17+
```
18+
1419
## CLI
1520

1621
At its simplest, the CLI is a pass-through validator:

cli/src/lib.rs

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use anyhow::{anyhow, Result};
2+
use clap::{ArgAction, Parser, ValueEnum};
3+
use cql2::{Expr, Validator};
4+
use std::io::Read;
5+
6+
/// The CQL2 command-line interface.
7+
#[derive(Debug, Parser)]
8+
#[command(version, about, long_about = None)]
9+
pub struct Cli {
10+
/// The input CQL2
11+
///
12+
/// If not provided, or `-`, the CQL2 will be read from standard input. The
13+
/// type (json or text) will be auto-detected. To specify a format, use
14+
/// --input-format.
15+
input: Option<String>,
16+
17+
/// The input format.
18+
///
19+
/// If not provided, the format will be auto-detected from the input.
20+
#[arg(short, long)]
21+
input_format: Option<InputFormat>,
22+
23+
/// The output format.
24+
///
25+
/// If not provided, the format will be the same as the input.
26+
#[arg(short, long)]
27+
output_format: Option<OutputFormat>,
28+
29+
/// Validate the CQL2
30+
#[arg(long, default_value_t = true, action = ArgAction::Set)]
31+
validate: bool,
32+
33+
/// Verbosity.
34+
///
35+
/// Provide this argument several times to turn up the chatter.
36+
#[arg(short, long, action = ArgAction::Count)]
37+
verbose: u8,
38+
}
39+
40+
/// The input CQL2 format.
41+
#[derive(Debug, ValueEnum, Clone)]
42+
pub enum InputFormat {
43+
/// cql2-json
44+
Json,
45+
46+
/// cql2-text
47+
Text,
48+
}
49+
50+
/// The output CQL2 format.
51+
#[derive(Debug, ValueEnum, Clone)]
52+
enum OutputFormat {
53+
/// cql2-json, pretty-printed
54+
JsonPretty,
55+
56+
/// cql2-json, compact
57+
Json,
58+
59+
/// cql2-text
60+
Text,
61+
62+
/// SQL
63+
Sql,
64+
}
65+
66+
impl Cli {
67+
/// Runs the cli.
68+
///
69+
/// # Examples
70+
///
71+
/// ```
72+
/// use cql2_cli::Cli;
73+
/// use clap::Parser;
74+
///
75+
/// let cli = Cli::try_parse_from(&["cql2", "landsat:scene_id = 'LC82030282019133LGN00'"]).unwrap();
76+
/// cli.run();
77+
/// ```
78+
pub fn run(self) {
79+
if let Err(err) = self.run_inner() {
80+
eprintln!("{}", err);
81+
std::process::exit(1)
82+
}
83+
}
84+
85+
pub fn run_inner(self) -> Result<()> {
86+
let input = self
87+
.input
88+
.and_then(|input| if input == "-" { None } else { Some(input) })
89+
.map(Ok)
90+
.unwrap_or_else(read_stdin)?;
91+
let input_format = self.input_format.unwrap_or_else(|| {
92+
if input.starts_with('{') {
93+
InputFormat::Json
94+
} else {
95+
InputFormat::Text
96+
}
97+
});
98+
let expr: Expr = match input_format {
99+
InputFormat::Json => cql2::parse_json(&input)?,
100+
InputFormat::Text => match cql2::parse_text(&input) {
101+
Ok(expr) => expr,
102+
Err(err) => {
103+
return Err(anyhow!("[ERROR] Parsing error: {input}\n{err}"));
104+
}
105+
},
106+
};
107+
if self.validate {
108+
let validator = Validator::new().unwrap();
109+
let value = serde_json::to_value(&expr).unwrap();
110+
if let Err(error) = validator.validate(&value) {
111+
return Err(anyhow!(
112+
"[ERROR] Invalid CQL2: {input}\n{}",
113+
match self.verbose {
114+
0 => "For more detailed validation information, use -v".to_string(),
115+
1 => format!("For more detailed validation information, use -vv\n{error}"),
116+
2 =>
117+
format!("For more detailed validation information, use -vvv\n{error:#}"),
118+
_ => {
119+
let detailed_output = error.detailed_output();
120+
format!("{detailed_output:#}")
121+
}
122+
}
123+
));
124+
}
125+
}
126+
let output_format = self.output_format.unwrap_or(match input_format {
127+
InputFormat::Json => OutputFormat::Json,
128+
InputFormat::Text => OutputFormat::Text,
129+
});
130+
match output_format {
131+
OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr)?,
132+
OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr)?,
133+
OutputFormat::Text => print!("{}", expr.to_text()?),
134+
OutputFormat::Sql => serde_json::to_writer_pretty(std::io::stdout(), &expr.to_sql()?)?,
135+
}
136+
println!();
137+
Ok(())
138+
}
139+
}
140+
141+
fn read_stdin() -> Result<String> {
142+
let mut buf = String::new();
143+
std::io::stdin().read_to_string(&mut buf)?;
144+
Ok(buf)
145+
}

cli/src/main.rs

+3-118
Original file line numberDiff line numberDiff line change
@@ -1,121 +1,6 @@
1-
use clap::{ArgAction, Parser, ValueEnum};
2-
use cql2::{Expr, Validator};
3-
use std::io::Read;
4-
5-
#[derive(Debug, Parser)]
6-
struct Cli {
7-
/// The input CQL2
8-
///
9-
/// If not provided, or `-`, the CQL2 will be read from standard input. The
10-
/// type (json or text) will be auto-detected. To specify a format, use
11-
/// --input-format.
12-
input: Option<String>,
13-
14-
/// The input format.
15-
///
16-
/// If not provided, the format will be auto-detected from the input.
17-
#[arg(short, long)]
18-
input_format: Option<InputFormat>,
19-
20-
/// The output format.
21-
///
22-
/// If not provided, the format will be the same as the input.
23-
#[arg(short, long)]
24-
output_format: Option<OutputFormat>,
25-
26-
/// Validate the CQL2
27-
#[arg(long, default_value_t = true, action = ArgAction::Set)]
28-
validate: bool,
29-
30-
/// Verbosity.
31-
///
32-
/// Provide this argument several times to turn up the chatter.
33-
#[arg(short, long, action = ArgAction::Count)]
34-
verbose: u8,
35-
}
36-
37-
#[derive(Debug, ValueEnum, Clone)]
38-
enum InputFormat {
39-
/// cql2-json
40-
Json,
41-
42-
/// cql2-text
43-
Text,
44-
}
45-
46-
#[derive(Debug, ValueEnum, Clone)]
47-
enum OutputFormat {
48-
/// cql2-json, pretty-printed
49-
JsonPretty,
50-
51-
/// cql2-json, compact
52-
Json,
53-
54-
/// cql2-text
55-
Text,
56-
57-
/// SQL
58-
Sql,
59-
}
1+
use clap::Parser;
2+
use cql2_cli::Cli;
603

614
fn main() {
62-
let cli = Cli::parse();
63-
let input = cli
64-
.input
65-
.and_then(|input| if input == "-" { None } else { Some(input) })
66-
.unwrap_or_else(read_stdin);
67-
let input_format = cli.input_format.unwrap_or_else(|| {
68-
if input.starts_with('{') {
69-
InputFormat::Json
70-
} else {
71-
InputFormat::Text
72-
}
73-
});
74-
let expr: Expr = match input_format {
75-
InputFormat::Json => cql2::parse_json(&input).unwrap(),
76-
InputFormat::Text => match cql2::parse_text(&input) {
77-
Ok(expr) => expr,
78-
Err(err) => {
79-
eprintln!("[ERROR] Parsing error: {input}");
80-
eprintln!("{err}");
81-
std::process::exit(1)
82-
}
83-
},
84-
};
85-
if cli.validate {
86-
let validator = Validator::new().unwrap();
87-
let value = serde_json::to_value(&expr).unwrap();
88-
if let Err(error) = validator.validate(&value) {
89-
eprintln!("[ERROR] Invalid CQL2: {input}");
90-
match cli.verbose {
91-
0 => eprintln!("For more detailed validation information, use -v"),
92-
1 => eprintln!("For more detailed validation information, use -vv\n{error}"),
93-
2 => eprintln!("For more detailed validation information, use -vvv\n{error:#}"),
94-
_ => {
95-
let detailed_output = error.detailed_output();
96-
eprintln!("{detailed_output:#}");
97-
}
98-
}
99-
std::process::exit(1)
100-
}
101-
}
102-
let output_format = cli.output_format.unwrap_or(match input_format {
103-
InputFormat::Json => OutputFormat::Json,
104-
InputFormat::Text => OutputFormat::Text,
105-
});
106-
match output_format {
107-
OutputFormat::JsonPretty => serde_json::to_writer_pretty(std::io::stdout(), &expr).unwrap(),
108-
OutputFormat::Json => serde_json::to_writer(std::io::stdout(), &expr).unwrap(),
109-
OutputFormat::Text => print!("{}", expr.to_text().unwrap()),
110-
OutputFormat::Sql => {
111-
serde_json::to_writer_pretty(std::io::stdout(), &expr.to_sql().unwrap()).unwrap()
112-
}
113-
}
114-
println!()
115-
}
116-
117-
fn read_stdin() -> String {
118-
let mut buf = String::new();
119-
std::io::stdin().read_to_string(&mut buf).unwrap();
120-
buf
5+
Cli::parse().run()
1216
}

0 commit comments

Comments
 (0)