Skip to content
Open
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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ KQL CLI — query Azure Data Explorer (Kusto) from the command line.
Like `jq` for JSON, but for Kusto/KQL. Run raw KQL, keep a git-versioned library
of parameterized queries, and pipe results straight into your shell.

![kq demo](docs/demo.gif)

## Installation

```bash
Expand Down
118 changes: 118 additions & 0 deletions docs/LAUNCH_POST.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
# Launch post drafts

Three formats below: a blog/dev.to post, short Reddit/forum variants, and a
one-line social post. Pick per venue; they say the same thing at different
lengths.

---

## A) Blog / dev.to — "I built jq for Kusto"

**Title:** I built `jq` for Kusto — a tiny CLI for querying Azure Data Explorer

I run a lot of KQL against Azure Data Explorer, and I kept hitting the same
friction: the web UI is great for exploring but terrible for automation, and
`az kusto` isn't built for quick, pipeable, everyday queries. I wanted the thing
`jq` is for JSON — small, fast, lives in the terminal, composes with pipes.

So I built **`kq`**.

```bash
pip install kql-cli # the command is `kq`

kq config set default_cluster https://mycluster.kusto.windows.net
kq config set default_database mydb
kq auth login

kq "MyTable | take 5"
kq "MyTable | summarize count() by bin(Timestamp, 1h)" -f json | jq .
```

The part I actually care about most is **saved, git-versioned queries**. You
write parameterized queries in YAML and run them by name:

```yaml
# ~/.config/kq/queries/ops.yaml
queries:
- name: errors
description: Recent errors for a service
parameters:
- {name: service, required: true}
- {name: hours, default: "24"}
query: |
Traces
| where Timestamp > ago({hours}h)
| where Service == '{service}'
| where Level == 'Error'
| order by Timestamp desc
```

```bash
kq run ops.errors checkout 6
```

Queries resolve from a project-local `./.kq/`, then your user queries, then
bundled examples — so a repo can ship its own query library, and your personal
queries never get clobbered by an update.

A few other things it does:

- **Auth that just works** — service principal → Azure CLI → cached device code,
with ~90-day silent refresh (handy on WSL/headless).
- **`table` / `json` / `csv`** output — the JSON pipes straight into `jq`.
- **Query safety levels** (`safe` / `caution` / `dangerous`) to keep expensive
full-table scans honest.
- **Plays nicely with LLM coding agents** — clean, deterministic output.

It's MIT-licensed, tested on Python 3.9–3.13, and on PyPI as `kql-cli`.

Repo: https://github.com/cptfinch/kq

If you work with Kusto/ADX, I'd love feedback — especially on what saved-query
patterns you'd want built in.

---

## B) Reddit / Hacker News "Show" variants

**Title options**
- Show: kq — jq for Kusto/KQL (query Azure Data Explorer from the CLI)
- I built a small CLI for querying Azure Data Explorer (like jq, but for KQL)

**Body (r/AZURE, r/dataengineering, r/kusto):**

I kept wanting a `jq`-style tool for KQL — something small and pipeable for
querying Azure Data Explorer from the terminal, instead of the web UI or
`az kusto`. So I made `kq`.

- `kq "MyTable | take 5"` for raw KQL, `-f json|csv` for output
- Saved, parameterized queries in YAML that you can git-version per project
- Auth chain: service principal → Azure CLI → cached device code (~90-day refresh)
- Query safety levels to flag expensive scans

`pip install kql-cli` (the command is `kq`). MIT, tested on 3.9–3.13.

https://github.com/cptfinch/kq

Curious what saved-query patterns people would find useful — and how others are
scripting against ADX today.

---

## C) One-liner (X / Mastodon / LinkedIn)

`kq` — jq, but for Kusto/KQL. Query Azure Data Explorer from the terminal: raw
KQL, git-versioned saved queries, json/csv output, sane auth.

`pip install kql-cli` · MIT · https://github.com/cptfinch/kq

---

## Posting notes

- Best-fit communities: r/AZURE, r/dataengineering, r/kusto, the Azure Data
Explorer community, and Show HN.
- Post *after* PyPI is live so `pip install kql-cli` works from the first click.
- The README demo GIF (`docs/demo.gif`) is worth embedding in the dev.to post
too — a moving demo roughly doubles conversion.
- Lead with the `jq` analogy every time; it's the fastest way people "get it."
56 changes: 56 additions & 0 deletions docs/RELEASE_NOTES_v1.0.0.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# kq v1.0.0 — first public release

Paste this into the GitHub Release body when tagging `v1.0.0`.

---

`kq` is a KQL CLI for Azure Data Explorer (Kusto). Like `jq` for JSON, but for
KQL — run raw queries, keep a git-versioned library of parameterized queries,
and pipe results straight into your shell.

## Install

```bash
pip install kql-cli
```

The command is `kq`. (The PyPI package is `kql-cli` because `kq` was already
taken on PyPI by an unrelated project.)

## Quick start

```bash
kq config set default_cluster https://mycluster.westeurope.kusto.windows.net
kq config set default_database mydb
kq auth login

kq "MyTable | take 5" # raw KQL
kq "MyTable | take 5" -f json # or -f csv
kq list # saved queries
kq run examples.sample MyTable 10 # run a saved query
```

## Highlights

- **Raw KQL and saved queries** — run KQL directly, or curate reusable,
parameterized queries in YAML and run them by name.
- **Git-versionable query libraries** — queries resolve from `./.kq/` (project),
`~/.config/kq/queries/` (user), then bundled examples. Your queries are never
overwritten by updates.
- **Multi-cluster config** — named clusters, XDG-compliant config in
`~/.config/kq/config.yaml`.
- **Flexible auth** — service principal → Azure CLI → cached device code, with
~90-day silent token refresh (great for WSL/headless).
- **Output formats** — `table`, `json`, `csv`.
- **Query safety levels** — mark queries `safe` / `caution` / `dangerous` to
keep expensive full-table scans honest.
- **LLM-native & Unix-friendly** — clean, pipeable output that works well with
Claude Code, Copilot, scripts, and automation.

## Quality

- Test suite + `ruff` linting.
- GitHub Actions CI across Python 3.9–3.13.
- Published via PyPI Trusted Publishing (OIDC — no stored tokens).

**Full changelog:** see [CHANGELOG.md](../CHANGELOG.md).
98 changes: 98 additions & 0 deletions docs/demo.cast
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
{"version": 2, "width": 94, "height": 34, "env": {"TERM": "xterm-256color", "SHELL": "/bin/bash"}, "title": "kq \u2014 jq for Kusto/KQL"}
[0.6, "o", "\u001b[36m~/kq\u001b[0m \u001b[32m$\u001b[0m "]
[0.645, "o", "k"]
[0.69, "o", "q"]
[0.735, "o", " "]
[0.78, "o", "-"]
[0.825, "o", "-"]
[0.87, "o", "v"]
[0.915, "o", "e"]
[0.96, "o", "r"]
[1.005, "o", "s"]
[1.05, "o", "i"]
[1.095, "o", "o"]
[1.14, "o", "n"]
[1.49, "o", "\r\n"]
[1.64, "o", "kq 1.0.0\r\n"]
[3.44, "o", "\u001b[36m~/kq\u001b[0m \u001b[32m$\u001b[0m "]
[3.485, "o", "k"]
[3.53, "o", "q"]
[3.575, "o", " "]
[3.62, "o", "l"]
[3.665, "o", "i"]
[3.71, "o", "s"]
[3.755, "o", "t"]
[4.105, "o", "\r\n"]
[4.255, "o", "\u001b[3m \u001b[0m\u001b[1;3mexamples\u001b[0m\u001b[3m \u001b[0m\r\n\u250f\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2533\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2513\r\n\u2503\u001b[1m \u001b[0m\u001b[1mQuery \u001b[0m\u001b[1m \u001b[0m\u2503\u001b[1m \u001b[0m\u001b[1mDescription \u001b[0m\u001b[1m \u001b[0m\u2503\u001b[1m \u001b[0m\u001b[1mParams \u001b[0m\u001b[1m \u001b[0m\u2503\r\n\u2521\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2547\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2529\r\n\u2502\u001b[36m \u001b[0m\u001b[36mcolumns\u001b[0m\u001b[36m \u001b[0m\u2502 List distinct values in a column \u2502\u001b[2m \u001b[0m\u001b[2mtable, column, hours\u001b[0m\u001b[2m \u001b[0m\u2502\r\n\u2502\u001b[36m \u001b[0m\u001b[36mcount \u001b[0m\u001b[36m \u001b[0m\u2502 Count rows in a table (with optional time filter) \u2502\u001b[2m \u001b[0m\u001b[2mtable, hours \u001b[0m\u001b[2m \u001b[0m\u2502\r\n\u2502\u001b[36m \u001b[0m\u001b[36msample \u001b[0m\u001b[36m \u001b[0m\u2502 Get sample rows from a table \u2502\u001b[2m \u001b[0m\u001b[2mtable, count \u001b[0m\u001b[2m \u001b[0m\u2502\r\n\u2502\u001b[36m \u001b[0m\u001b[36mschema \u001b[0m\u001b[36m \u001b[0m\u2502 Show schema for a table \u2502\u001b[2m \u001b[0m\u001b[2mtable \u001b[0m\u001b[2m \u001b[0m\u2502\r\n\u2502\u001b[36m \u001b[0m\u001b[36mtables \u001b[0m\u001b[36m \u001b[0m\u2502 List all tables in the database \u2502\u001b[2m \u001b[0m\u001b[2m- \u001b[0m\u001b[2m \u001b[0m\u2502\r\n\u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\r\n\r\n"]
[6.055, "o", "\u001b[36m~/kq\u001b[0m \u001b[32m$\u001b[0m "]
[6.1, "o", "k"]
[6.145, "o", "q"]
[6.19, "o", " "]
[6.235, "o", "s"]
[6.28, "o", "h"]
[6.325, "o", "o"]
[6.37, "o", "w"]
[6.415, "o", " "]
[6.46, "o", "e"]
[6.505, "o", "x"]
[6.55, "o", "a"]
[6.595, "o", "m"]
[6.64, "o", "p"]
[6.685, "o", "l"]
[6.73, "o", "e"]
[6.775, "o", "s"]
[6.82, "o", "."]
[6.865, "o", "s"]
[6.91, "o", "a"]
[6.955, "o", "m"]
[7.0, "o", "p"]
[7.045, "o", "l"]
[7.09, "o", "e"]
[7.44, "o", "\r\n"]
[7.59, "o", "\u256d\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256e\r\n\u2502 \u001b[1mexamples.sample\u001b[0m \u2502\r\n\u2570\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500 safe \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u256f\r\n\u001b[2mDescription:\u001b[0m Get sample rows from a table\r\n\u001b[2mSource:\u001b[0m \u001b[35m/home/user/kq/src/kq/queries/\u001b[0m\u001b[95mexamples.yaml\u001b[0m\r\n\r\n\u001b[2mParameters:\u001b[0m\r\n \u001b[31m*\u001b[0mtable: Table name\r\n count: Number of rows \u001b[1m(\u001b[0mdefault: \u001b[1;36m10\u001b[0m\u001b[1m)\u001b[0m\r\n\r\n\u001b[2mQuery:\u001b[0m\r\n\u001b[38;5;198;48;5;16m{\u001b[0m\u001b[38;5;81;48;5;235mtable\u001b[0m\u001b[38;5;198;48;5;16m}\u001b[0m\u001b[48;5;235m \u001b[0m\r\n\u001b[38;5;204;48;5;235m|\u001b[0m\u001b[38;5;231;48;5;235m \u001b[0m\u001b[38;5;231;48;5;235mtake\u001b[0m\u001b[38;5;231;48;5;235m \u001b[0m\u001b[38;5;198;48;5;16m{\u001b[0m\u001b[38;5;81;48;5;235mcount\u001b[0m\u001b[38;5;198;48;5;16m}\u001b[0m\u001b[48;5;235m \u001b[0m\r\n\u001b[48;5;235m \u001b[0m\r\n\r\n\u001b[2mExample:\u001b[0m kq run examples.sample MyTable \u001b[1;36m5\u001b[0m\r\n"]
[9.39, "o", "\u001b[36m~/kq\u001b[0m \u001b[32m$\u001b[0m "]
[9.435, "o", "k"]
[9.48, "o", "q"]
[9.525, "o", " "]
[9.57, "o", "r"]
[9.615, "o", "u"]
[9.66, "o", "n"]
[9.705, "o", " "]
[9.75, "o", "e"]
[9.795, "o", "x"]
[9.84, "o", "a"]
[9.885, "o", "m"]
[9.93, "o", "p"]
[9.975, "o", "l"]
[10.02, "o", "e"]
[10.065, "o", "s"]
[10.11, "o", "."]
[10.155, "o", "s"]
[10.2, "o", "a"]
[10.245, "o", "m"]
[10.29, "o", "p"]
[10.335, "o", "l"]
[10.38, "o", "e"]
[10.425, "o", " "]
[10.47, "o", "M"]
[10.515, "o", "y"]
[10.56, "o", "T"]
[10.605, "o", "a"]
[10.65, "o", "b"]
[10.695, "o", "l"]
[10.74, "o", "e"]
[10.785, "o", " "]
[10.83, "o", "5"]
[10.875, "o", " "]
[10.92, "o", "-"]
[10.965, "o", "-"]
[11.01, "o", "d"]
[11.055, "o", "r"]
[11.1, "o", "y"]
[11.145, "o", "-"]
[11.19, "o", "r"]
[11.235, "o", "u"]
[11.28, "o", "n"]
[11.63, "o", "\r\n"]
[11.78, "o", "\u001b[2mQuery \u001b[0m\u001b[1;2m(\u001b[0m\u001b[2mdry run\u001b[0m\u001b[1;2m)\u001b[0m\u001b[2m:\u001b[0m\r\n\u001b[38;5;231;48;5;235mMyTable\u001b[0m\u001b[48;5;235m \u001b[0m\r\n\u001b[38;5;204;48;5;235m|\u001b[0m\u001b[38;5;231;48;5;235m \u001b[0m\u001b[38;5;231;48;5;235mtake\u001b[0m\u001b[38;5;231;48;5;235m \u001b[0m\u001b[38;5;141;48;5;235m5\u001b[0m\u001b[48;5;235m \u001b[0m\r\n"]
[14.28, "o", "\u001b[36m~/kq\u001b[0m \u001b[32m$\u001b[0m "]
Binary file added docs/demo.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading