Skip to content
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
53 changes: 53 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true

jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install ruff
run: pip install ruff
- name: Lint
run: ruff check src tests

test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install package with dev extras
run: pip install -e ".[dev]"
- name: Run tests
run: pytest -q

build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build sdist and wheel
run: |
pip install build twine
python -m build
twine check dist/*
46 changes: 46 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
name: Release

# Publishes to PyPI when a version tag (e.g. v1.0.0) is pushed.
#
# Uses PyPI Trusted Publishing (OIDC) — no API token or secret is stored in
# the repo. One-time setup on PyPI: create the "kql-cli" project's trusted
# publisher pointing at this repo, workflow "release.yml", environment "pypi".
# See: https://docs.pypi.org/trusted-publishers/

on:
push:
tags:
- "v*"
workflow_dispatch:

jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build
run: |
pip install build twine
python -m build
twine check dist/*
- uses: actions/upload-artifact@v4
with:
name: dist
path: dist/

publish:
needs: build
runs-on: ubuntu-latest
environment: pypi
permissions:
id-token: write # required for Trusted Publishing (OIDC)
steps:
- uses: actions/download-artifact@v4
with:
name: dist
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
7 changes: 5 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ htmlcov/
# Build
*.whl

# IBA-specific queries live in the private knowledge-base repo, not here
# IBA-specific queries live in the private knowledge-base repo, not here.
# Anchored to the repo root so it never matches the bundled package data
# in src/kq/queries/.
KQL snippets.md
*.snippets.md
queries/
/queries/
/.kq/
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
# Changelog

All notable changes to this project are documented here. The format is based on
[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [1.0.0] - 2026-07-07

First public release on PyPI.

### Added
- Query Azure Data Explorer (Kusto) from the command line: raw KQL, saved
parameterized queries, and bundled examples.
- Multi-cluster configuration with XDG-compliant config in
`~/.config/kq/config.yaml`.
- Layered query resolution: project-local `.kq/`, user `~/.config/kq/queries/`,
then bundled examples.
- Authentication chain: service principal → Azure CLI → cached device code
(with ~90-day silent refresh).
- Output formats: `table`, `json`, `csv`.
- Query safety levels (`safe` / `caution` / `dangerous`).
- Test suite, `ruff` linting, GitHub Actions CI (Python 3.9–3.13), and a
Trusted-Publishing release workflow.

### Note
- The tool is distributed on PyPI as **`kql-cli`** (the command remains `kq`),
because the `kq` name on PyPI is taken by an unrelated project.

[1.0.0]: https://github.com/cptfinch/kq/releases/tag/v1.0.0
21 changes: 21 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 cptfinch

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
45 changes: 41 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,24 @@
# kq

KQL CLI - Query Azure Data Explorer from the command line.
[![CI](https://github.com/cptfinch/kq/actions/workflows/ci.yml/badge.svg)](https://github.com/cptfinch/kq/actions/workflows/ci.yml)
[![PyPI](https://img.shields.io/pypi/v/kql-cli.svg)](https://pypi.org/project/kql-cli/)
[![Python versions](https://img.shields.io/pypi/pyversions/kql-cli.svg)](https://pypi.org/project/kql-cli/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Like `jq` for JSON, but for Kusto/KQL.
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.

## Installation

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

> The command you run is `kq`. The PyPI **package** is named `kql-cli` because
> `kq` was already taken on PyPI by an unrelated project.

Or from source:

```bash
Expand Down Expand Up @@ -159,6 +168,34 @@ MyTable | where Category == 'Error'
- **Unix-friendly** - Pipes, scripts, automation
- **Personal queries** - User queries never overwritten by updates

## Development

```bash
git clone https://github.com/cptfinch/kq.git
cd kq
python -m venv .venv && . .venv/bin/activate
pip install -e ".[dev]"

pytest # run tests
ruff check . # lint
python -m build # build sdist + wheel
```

CI runs lint + tests across Python 3.9–3.13 on every push and pull request.

### Releasing

Releases publish to PyPI automatically via
[Trusted Publishing](https://docs.pypi.org/trusted-publishers/) (OIDC — no
tokens stored in the repo). To cut a release:

1. Bump `__version__` in `src/kq/__init__.py` and update `CHANGELOG.md`.
2. Tag and push: `git tag v1.2.3 && git push origin v1.2.3`.

The `release.yml` workflow builds the artifacts and publishes them. This
requires a one-time PyPI setup: configure `kql-cli`'s trusted publisher to point
at this repository, workflow `release.yml`, environment `pypi`.

## License

MIT
MIT — see [LICENSE](LICENSE).
40 changes: 30 additions & 10 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,23 @@ requires = ["hatchling"]
build-backend = "hatchling.build"

[project]
name = "kq"
version = "0.1.0"
description = "KQL CLI - Query Azure Data Explorer from the command line"
name = "kql-cli"
dynamic = ["version"]
description = "KQL CLI - query Azure Data Explorer (Kusto) from the command line. Like jq, but for KQL."
readme = "README.md"
license = "MIT"
license-files = ["LICENSE"]
requires-python = ">=3.9"
authors = [
{ name = "Your Name" }
{ name = "cptfinch", email = "cptfinch@gmail.com" },
]
keywords = ["kql", "kusto", "azure", "data-explorer", "adx", "cli"]
keywords = ["kql", "kusto", "azure", "data-explorer", "adx", "cli", "jq"]
classifiers = [
"Development Status :: 4 - Beta",
"Development Status :: 5 - Production/Stable",
"Environment :: Console",
"License :: OSI Approved :: MIT License",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
Expand All @@ -37,24 +40,41 @@ dependencies = [
dev = [
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"ruff>=0.5.0",
"build>=1.0.0",
]

[project.scripts]
kq = "kq.cli:main"

[project.urls]
Homepage = "https://github.com/cptfinch/kq"
Documentation = "https://github.com/cptfinch/kq#readme"
Source = "https://github.com/cptfinch/kq"
Issues = "https://github.com/cptfinch/kq/issues"
Changelog = "https://github.com/cptfinch/kq/blob/main/CHANGELOG.md"

[tool.hatch.version]
path = "src/kq/__init__.py"

[tool.hatch.build.targets.wheel]
packages = ["src/kq"]

[tool.hatch.build.targets.wheel.force-include]
"src/kq/queries" = "kq/queries"

[tool.hatch.build.targets.sdist]
include = [
"/src",
"/README.md",
"/LICENSE",
"/CHANGELOG.md",
]

[tool.pytest.ini_options]
testpaths = ["tests"]
addopts = "-ra"

[tool.ruff]
line-length = 100
target-version = "py39"

[tool.ruff.lint]
select = ["E", "F", "I", "UP", "B"]
2 changes: 1 addition & 1 deletion src/kq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,4 @@
kq "PLC | take 5" # Run raw KQL
"""

__version__ = "0.1.0"
__version__ = "1.0.0"
8 changes: 4 additions & 4 deletions src/kq/auth.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
# Suppress noisy azure-identity credential chain warnings
logging.getLogger("azure.identity").setLevel(logging.ERROR)

from azure.identity import (
AzureCliCredential,
from azure.identity import ( # noqa: E402 (imported after logger is quieted)
AuthenticationRecord,
AzureCliCredential,
ClientSecretCredential,
DeviceCodeCredential,
TokenCachePersistenceOptions,
Expand Down Expand Up @@ -135,10 +135,10 @@ def prompt_callback(url, code, expires_on):
else:
expires = str(token.expires_on)

print(f"\nAuthentication successful!")
print("\nAuthentication successful!")
print(f" Expires: {expires}")
print(f" Auth record saved to {AUTH_RECORD_PATH}")
print(f" Subsequent queries will authenticate silently")
print(" Subsequent queries will authenticate silently")
return True
except Exception as e:
print(f"Authentication failed: {e}", file=sys.stderr)
Expand Down
Loading
Loading