Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
102 commits
Select commit Hold shift + click to select a range
5c36246
add agents.md with contributor and agent working rules
bencehezso Oct 22, 2026
8f25ffd
Merge pull request #68 from escapecloud/task/agents_md
bencehezso Oct 22, 2026
ccc0e96
untrack config.py, add exit codes, refresh dependencies
bencehezso Oct 22, 2026
5ff39bd
Merge pull request #70 from escapecloud/task/cli2_foundation
bencehezso Oct 22, 2026
a6ff00b
add config file, key files and config show command
bencehezso Oct 22, 2026
9583f43
Merge pull request #71 from escapecloud/task/cli2_config
bencehezso Oct 22, 2026
1f267de
add catalogue manifest update and catalogue commands
bencehezso Oct 22, 2026
d7ce81e
Merge pull request #72 from escapecloud/task/cli2_catalogue
bencehezso Oct 22, 2026
f165af9
build sync.json from the api v2 definition
bencehezso Oct 23, 2026
db0505f
Merge pull request #73 from escapecloud/task/cli2_sync_document
bencehezso Oct 23, 2026
09ff018
always write the local report and record upload links
bencehezso Oct 23, 2026
1ce6f52
Merge pull request #74 from escapecloud/task/cli2_report_flow
bencehezso Oct 23, 2026
97110c6
replace the pdf with the basic executive summary
bencehezso Oct 23, 2026
bda7cb3
Merge pull request #76 from escapecloud/task/cli2_basic_report
bencehezso Oct 23, 2026
c5138e9
replace the v1 client with the api v2 client
bencehezso Oct 24, 2026
7367f02
Merge pull request #77 from escapecloud/task/cli2_v2_client
bencehezso Oct 24, 2026
b208b18
ship report assets inside the package
bencehezso Oct 24, 2026
139075e
Merge pull request #78 from escapecloud/task/cli2_packaging
bencehezso Oct 24, 2026
1a77eb7
refresh dependencies and move to the new azure sdks
bencehezso Oct 24, 2026
553a55d
count azure resources without a location as global
bencehezso Oct 24, 2026
d32a8db
mark release/2.0 builds as 2.0.0.dev0
bencehezso Oct 24, 2026
c64a6d8
Merge pull request #79 from escapecloud/task/cli2_dependency_refresh
bencehezso Oct 24, 2026
e90f9ef
Merge pull request #80 from escapecloud/task/cli2_dev_version
bencehezso Oct 24, 2026
c9cddb6
make azure imports lazy and fix upgrades from 1.x
bencehezso Oct 24, 2026
e553a93
Merge pull request #81 from escapecloud/task/cli2_azure_upgrade_fix
bencehezso Oct 24, 2026
94cc828
replace shared icon images with a drawn placeholder
bencehezso Oct 24, 2026
9f1cd10
Merge pull request #82 from escapecloud/task/cli2_remove_shared_icons
bencehezso Oct 24, 2026
3674972
remove unused svg icons, favicon and medium font
bencehezso Oct 24, 2026
2fcefb7
remove the legacy pdf code and utils_report_pdf
bencehezso Oct 24, 2026
ce86bb4
Merge pull request #84 from escapecloud/task/cli2_dead_code_cleanup
bencehezso Oct 24, 2026
369eb28
fix the issues found in the first platform test run
bencehezso Oct 24, 2026
e2b1bdc
Merge pull request #85 from escapecloud/task/cli2_test_run_fixes
bencehezso Oct 24, 2026
ec5bc9f
retry uploads with backoff and spool what cannot be sent
bencehezso Oct 24, 2026
0cb75f6
Merge pull request #86 from escapecloud/task/cli2_retry_spool
bencehezso Oct 24, 2026
ee55356
add upload modes, strict, json output and status
bencehezso Oct 24, 2026
c9bd0b7
Merge pull request #87 from escapecloud/task/cli2_upload_modes
bencehezso Oct 24, 2026
41353e0
test api v2 contract, dataset shapes and a python matrix
bencehezso Oct 24, 2026
8f9c59e
fix the windows test run: utf-8 output, run.log, line endings
bencehezso Oct 24, 2026
8a7c809
fix toml paths in the settings tests on windows
bencehezso Oct 24, 2026
7200d63
Merge pull request #88 from escapecloud/task/cli2_test_hardening
bencehezso Oct 24, 2026
06e9354
move tests into folders that mirror the source layout
bencehezso Oct 24, 2026
90d9dd1
update test imports, paths and discovery for the new layout
bencehezso Oct 24, 2026
c5cae39
Merge pull request #89 from escapecloud/task/cli2_reorg_tests
bencehezso Oct 24, 2026
90e39a5
move version, codes, resources and assets into cloudexit
bencehezso Oct 24, 2026
c18a82e
update imports, packaging and CI for the cloudexit package
bencehezso Oct 24, 2026
c8252af
Merge pull request #90 from escapecloud/task/cli2_reorg_package
bencehezso Oct 24, 2026
82e50f6
move settings, validate and constants into cloudexit
bencehezso Oct 24, 2026
58b3a45
point importers at runconfig and settings, split the loader
bencehezso Oct 24, 2026
ba28ae3
Merge pull request #91 from escapecloud/task/cli2_reorg_runconfig
bencehezso Oct 24, 2026
080a78a
move the catalogue store and db helpers into cloudexit
bencehezso Oct 24, 2026
197e133
point importers and patch targets at cloudexit.catalogue
bencehezso Oct 24, 2026
a7709ab
Merge pull request #92 from escapecloud/task/cli2_reorg_catalogue
bencehezso Oct 24, 2026
d509d37
move the aws and azure collectors into cloudexit.providers
bencehezso Oct 24, 2026
846a2a7
split the collectors, point importers and tests at providers
bencehezso Oct 24, 2026
d6f6468
Merge pull request #93 from escapecloud/task/cli2_reorg_providers
bencehezso Oct 24, 2026
ace3e64
move the egress model and tfstate into cloudexit
bencehezso Oct 24, 2026
33938eb
split the egress model from the estimate, add a layering test
bencehezso Oct 24, 2026
f82bb96
Merge pull request #94 from escapecloud/task/cli2_reorg_egress
bencehezso Oct 24, 2026
e319418
fold the flat report modules into cloudexit.report
bencehezso Oct 24, 2026
7b16bb3
move the egress pdf builders, point importers at cloudexit.report
bencehezso Oct 24, 2026
20157ca
Merge pull request #96 from escapecloud/task/cli2_reorg_report
bencehezso Oct 24, 2026
66f4233
move the api client, document, upload and spool to platform
bencehezso Oct 24, 2026
0618ff4
point importers and patch targets at cloudexit.platform
bencehezso Oct 24, 2026
7592f8d
Merge pull request #97 from escapecloud/task/cli2_reorg_platform
bencehezso Oct 24, 2026
13d41ba
move the engine into cloudexit
bencehezso Oct 24, 2026
f0a3cdf
point importers and patch targets at cloudexit.engine
bencehezso Oct 24, 2026
414c4ec
Merge pull request #98 from escapecloud/task/cli2_reorg_engine
bencehezso Oct 24, 2026
83d81ff
move main.py and the prompts into cloudexit.cli
bencehezso Oct 24, 2026
67ffb05
switch the entry point to cloudexit.cli, drop core and utils
bencehezso Oct 24, 2026
d01ede8
Merge pull request #99 from escapecloud/task/cli2_reorg_cli
bencehezso Oct 24, 2026
0f3c854
move print_step, ask and the console to cloudexit.terminal
bencehezso Oct 24, 2026
b4c0a99
drop the lazy cli import, add a package layering test
bencehezso Oct 24, 2026
7bbefab
drop the lazy cli import, add a package layering test
bencehezso Oct 24, 2026
d41dc62
Merge pull request #100 from escapecloud/task/cli2_terminal_module
bencehezso Oct 24, 2026
a71eccb
name every logger after its module
bencehezso Oct 24, 2026
26c6cc6
Merge pull request #101 from escapecloud/task/cli2_logger_names
bencehezso Oct 24, 2026
921f30b
split the argument parser out of cli.app
bencehezso Oct 24, 2026
d18038e
Merge pull request #102 from escapecloud/task/cli2_cli_parser
bencehezso Oct 24, 2026
8f70cda
split the catalogue download code out of the store
bencehezso Oct 24, 2026
df3d1dd
Merge pull request #103 from escapecloud/task/cli2_catalogue_download
bencehezso Oct 24, 2026
046d23a
stop writing credential identifiers into the egress pdf and run.log
bencehezso Oct 25, 2026
64779f5
Merge pull request #104 from escapecloud/task/report_egress_no_creden…
bencehezso Oct 25, 2026
574e5d8
end every executive summary with legal notices, mark the edition
bencehezso Oct 25, 2026
5a53008
Merge pull request #106 from escapecloud/task/report_legal_notices
bencehezso Oct 25, 2026
cdc3a14
rebuild the egress pdf as the egress and data landscape companion doc…
bencehezso Oct 25, 2026
6cbb413
Merge pull request #107 from escapecloud/task/report_egress_redesign
bencehezso Oct 25, 2026
93359c8
add a sample report matrix rendered through the real pipeline
bencehezso Oct 25, 2026
fc7b284
Merge pull request #108 from escapecloud/task/report_sample_matrix
bencehezso Oct 25, 2026
34656da
replace the two html reports with one offline page
bencehezso Oct 25, 2026
98b2d54
keep line endings of the bundled driver.js files as they are
bencehezso Oct 25, 2026
fdbe780
Merge pull request #109 from escapecloud/task/report_html_single_page
bencehezso Oct 25, 2026
a649387
drop the pdf closing page and state failed uploads and skipped state …
bencehezso Oct 25, 2026
1304b88
Merge pull request #110 from escapecloud/task/report_fine_tuning
bencehezso Oct 25, 2026
85ea728
harden error output, spool keys, report files and catalogue
bencehezso Oct 25, 2026
a9fd198
write the report logo path with forward slashes
bencehezso Oct 25, 2026
00a5de0
Merge pull request #111 from escapecloud/task/cli2_security_fixes
bencehezso Oct 25, 2026
36bb644
remove the legacy settings sources, the output promotion and the cred…
bencehezso Oct 25, 2026
3716c2d
offer the hybrid cloud exit strategy in the prompt and in ESC_EXIT_ST…
bencehezso Oct 25, 2026
0afcc4a
Merge pull request #112 from escapecloud/task/release_blockers_and_po…
bencehezso Oct 25, 2026
c296039
move the platform reference to the docs site and add the 2.0 docs
bencehezso Oct 25, 2026
a46e2e4
Merge pull request #113 from escapecloud/task/cli2_docs
bencehezso Oct 25, 2026
fec7dbd
Release v2.0.0-rc.1
github-actions[bot] Oct 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
7 changes: 7 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# The definition copy is compared by file hash, so it must not get CRLF line endings
docs/api-v2/** text eol=lf

# The bundled driver.js files are checked against a pinned hash and shipped
# unmodified, so no line ending conversion may touch them
cloudexit/assets/js/** -text
cloudexit/assets/css/driver.css -text
170 changes: 165 additions & 5 deletions .github/workflows/pr-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
pip install -r requirements-dev.txt

- name: Ruff
run: ruff check main.py core utils tests
run: ruff check main.py cloudexit tests

check-types:
if: github.event.pull_request.draft == false
Expand All @@ -49,18 +49,28 @@ jobs:
pip install -r requirements-dev.txt

- name: Black
run: black --check main.py core utils tests
run: black --check main.py cloudexit tests

check-tests:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
name: tests (${{ matrix.os }}, Python ${{ matrix.python }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- { os: ubuntu-latest, python: "3.12" }
- { os: ubuntu-latest, python: "3.13" }
- { os: ubuntu-latest, python: "3.14", coverage: true }
- { os: macos-latest, python: "3.14" }
- { os: windows-latest, python: "3.14" }
steps:
- uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}
python-version: ${{ matrix.python }}
cache: pip

- name: Install dependencies
Expand All @@ -69,4 +79,154 @@ jobs:
pip install -r requirements-dev.txt

- name: Unit tests
run: python -m unittest discover -s tests
if: ${{ !matrix.coverage }}
run: python -m unittest discover -s tests -t .

- name: Unit tests with coverage
if: ${{ matrix.coverage }}
run: |
coverage run -m unittest discover -s tests -t .
coverage report

install-smoke:
if: github.event.pull_request.draft == false
name: install smoke (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
defaults:
run:
shell: bash
steps:
- uses: actions/checkout@v5

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Locate the virtual environment
run: |
if [ "$RUNNER_OS" = "Windows" ]; then bin=Scripts; else bin=bin; fi
echo "VENV_BIN=$RUNNER_TEMP/venv/$bin" >> "$GITHUB_ENV"

- name: Install the package (not editable)
run: |
python -m venv "$RUNNER_TEMP/venv"
"$VENV_BIN/python" -m pip install --upgrade pip
"$VENV_BIN/python" -m pip install .

- name: Build the smoke dataset
run: '"$VENV_BIN/python" -m tests.make_smoke_dataset "$RUNNER_TEMP/smoke.db"'

- name: Run from an unrelated directory
env:
ESC_EXIT_STRATEGY: "1"
ESC_ASSESSMENT_TYPE: "1"
run: |
cloudexit="$VENV_BIN/cloudexit"
workdir="$(mktemp -d)"
cd "$workdir"

"$cloudexit" --version
"$cloudexit" config path
"$VENV_BIN/python" - <<'PY'
from cloudexit.providers.azure.clients import (
create_cost_client,
create_resource_client,
create_subscription_client,
)


class Credential:
def get_token(self, *scopes, **kwargs):
raise RuntimeError("no network in this check")


create_resource_client(Credential(), "00000000-0000-0000-0000-000000000000")
create_subscription_client(Credential())
create_cost_client(Credential())
print("the Azure clients import and build")
PY
"$cloudexit" aws \
--tfstate "$GITHUB_WORKSPACE/tests/fixtures/smoke/aws.tfstate" \
--catalogue "$RUNNER_TEMP/smoke.db" \
--non-interactive

report="$(ls -d reports/*/ | head -n 1)"
test -f "${report}index.html"
test -f "${report}report.pdf"
test -d "${report}assets/css"
test -d "${report}assets/img"
test -n "$(find "${report}assets/icons/aws" -name '*.png' | head -n 1)"
test "$(wc -c < "${report}report.pdf")" -gt 10000

upgrade-smoke:
if: github.event.pull_request.draft == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
with:
fetch-depth: 0

- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: ${{ env.PYTHON_VERSION }}

- name: Install the 1.1.7 environment
run: |
python -m venv "$RUNNER_TEMP/venv"
"$RUNNER_TEMP/venv/bin/python" -m pip install --upgrade pip
git show v1.1.7:requirements.txt > "$RUNNER_TEMP/req-1.1.7.txt"
"$RUNNER_TEMP/venv/bin/python" -m pip install -r "$RUNNER_TEMP/req-1.1.7.txt"

- name: Upgrade it to this branch
run: '"$RUNNER_TEMP/venv/bin/python" -m pip install .'

- name: Build the smoke dataset
run: '"$RUNNER_TEMP/venv/bin/python" -m tests.make_smoke_dataset "$RUNNER_TEMP/smoke.db"'

- name: Run from an unrelated directory
env:
ESC_EXIT_STRATEGY: "1"
ESC_ASSESSMENT_TYPE: "1"
run: |
cloudexit="$RUNNER_TEMP/venv/bin/cloudexit"
workdir="$(mktemp -d)"
cd "$workdir"

"$cloudexit" --version
"$cloudexit" config path
"$RUNNER_TEMP/venv/bin/python" - <<'PY'
from cloudexit.providers.azure.clients import (
create_cost_client,
create_resource_client,
create_subscription_client,
)


class Credential:
def get_token(self, *scopes, **kwargs):
raise RuntimeError("no network in this check")


create_resource_client(Credential(), "00000000-0000-0000-0000-000000000000")
create_subscription_client(Credential())
create_cost_client(Credential())
print("the Azure clients import and build")
PY
"$cloudexit" aws \
--tfstate "$GITHUB_WORKSPACE/tests/fixtures/smoke/aws.tfstate" \
--catalogue "$RUNNER_TEMP/smoke.db" \
--non-interactive

report="$(ls -d reports/*/ | head -n 1)"
test -f "${report}index.html"
test -f "${report}report.pdf"
test -d "${report}assets/css"
test -d "${report}assets/img"
test -n "$(find "${report}assets/icons/aws" -name '*.png' | head -n 1)"
test "$(wc -c < "${report}report.pdf")" -gt 10000
49 changes: 28 additions & 21 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
workflow_dispatch:
inputs:
version:
description: "Version, no leading v (e.g. 1.1.5)"
description: "Version, no leading v (e.g. 1.1.5, or 2.0.0-rc.1 from a release branch)"
required: true

permissions:
Expand All @@ -18,31 +18,38 @@ jobs:
with:
token: ${{ secrets.RELEASE_TOKEN }}

- name: Guard against non-main dispatch
- name: Validate branch, version and guard against existing tag
env:
VERSION: ${{ inputs.version }}
run: |
if [ "${{ github.ref }}" != "refs/heads/main" ]; then
echo "Release must be dispatched from main, got ${{ github.ref }}"; exit 1
case "$GITHUB_REF" in
refs/heads/main)
echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "Final versions (X.Y.Z) only from main, got: $VERSION"; exit 1; }
;;
refs/heads/release/*)
echo "$VERSION" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$' || { echo "Only release candidates (X.Y.Z-rc.N) from a release branch, got: $VERSION"; exit 1; }
;;
*)
echo "Release must be dispatched from main or a release/* branch, got $GITHUB_REF"; exit 1
;;
esac
if git ls-remote --exit-code --tags origin "refs/tags/v$VERSION" >/dev/null 2>&1; then
echo "Tag v$VERSION already exists on origin"; exit 1
fi

- name: Validate version and guard against existing tag
- name: Bump cloudexit/version.py
env:
VERSION: ${{ inputs.version }}
run: |
v="${{ inputs.version }}"
echo "$v" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$' || { echo "Bad version format: $v"; exit 1; }
if git ls-remote --exit-code --tags origin "refs/tags/v$v" >/dev/null 2>&1; then
echo "Tag v$v already exists on origin"; exit 1
fi

- name: Bump utils/version.py
run: |
v="${{ inputs.version }}"
sed -i "s/^__version__ = .*/__version__ = \"$v\"/" utils/version.py
grep -q "^__version__ = \"$v\"$" utils/version.py || { echo "sed did not update version"; exit 1; }
sed -i "s/^__version__ = .*/__version__ = \"$VERSION\"/" cloudexit/version.py
grep -q "^__version__ = \"$VERSION\"$" cloudexit/version.py || { echo "sed did not update version"; exit 1; }

- name: Commit, tag, and push to main
- name: Commit, tag, and push to the dispatched branch
env:
VERSION: ${{ inputs.version }}
run: |
v="${{ inputs.version }}"
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git commit -am "Release v$v"
git tag -a "v$v" -m "Release v$v"
git push origin HEAD "refs/tags/v$v"
git commit -am "Release v$VERSION"
git tag -a "v$VERSION" -m "Release v$VERSION"
git push origin HEAD "refs/tags/v$VERSION"
18 changes: 15 additions & 3 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,18 @@ web_modules/
# Generated reports
reports/

# Generated reports
config/aws.json
config/azure.json
# Rendered sample reports (python -m tests.samples)
/samples/

# Local configs (only the example templates are tracked)
config/*
!config/*_example.json

# Terraform state and downloaded datasets
*.tfstate
!tests/fixtures/smoke/aws.tfstate
!tests/fixtures/samples/tfstate/*.tfstate
datasets/

# TypeScript cache
*.tsbuildinfo
Expand Down Expand Up @@ -404,3 +413,6 @@ $RECYCLE.BIN/
output/
logs/
*.xlsx

# Local platform credentials (deprecated)
/config.py
72 changes: 72 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# AGENTS.md

## What this is

cloudexit is an offline-first CLI. It collects cloud inventory and cost data with the user's own credentials, builds a local report, and can optionally upload the results to a platform through API v2. Cloud credentials stay on the user's machine and are never sent anywhere.

## Setup

- Develop on Python 3.14. CI also tests 3.12 and 3.13 on Linux and 3.14 on macOS and Windows, because `requires-python` is `>=3.12`: don't use anything newer than 3.12 (for example `uuid.uuid7` only behind a fallback).
- Create and activate a virtualenv, then `pip install -r requirements-dev.txt`.
- Run the CLI with `python3 main.py`, or `pip install -e .` and use the `cloudexit` command.
- Local configs go in `config/` (start from a `*_example.json` template). They are never committed.

## Lint and format

CI runs exactly these, with the tool versions pinned in `requirements-dev.txt`:

```bash
ruff check main.py cloudexit tests
black --check main.py cloudexit tests
```

Fix with `ruff check --fix main.py cloudexit tests` and `black main.py cloudexit tests`. Don't add a `# noqa` or an ignore without a code comment saying why.

## Tests

```bash
python -m unittest discover -s tests -t .
```

- Add or update tests with every behaviour change. Tests sit in sub-directories that mirror the source layout (`tests/platform/`, `tests/report/`, ...); shared builders are in `tests/support/` and data files in `tests/fixtures/`.
- Always discover with `-t .`: without it `tests/` itself becomes the top-level directory and `tests/platform/` would be imported as `platform`, shadowing the standard library.
- Tests never call real cloud APIs or real platforms. Mock `boto3`, the Azure SDK and HTTP.
- Tests that cannot run on a platform (POSIX file modes on Windows) are skipped with `unittest.skipIf` or `skipUnless` and a reason, never deleted.
- The Ubuntu 3.14 job runs the suite under `coverage` and prints the report. There is no threshold.

## Branches

- Name branches `task/<snake_case>`, one topic per branch.
- During 2.0 development, branch from and target `release/2.0`. `main` only gets hotfixes and Dependabot PRs (Dependabot always targets `main`).
- Before each release candidate, bring `main` into `release/2.0` with a PR from a branch `task/sync_main` (`git merge origin/main`). Merge it with a merge commit, not a squash.
- After the 2.0 release, branch from and target `main`.

## Commits

One line, lowercase, no trailing period, plain words about what changed, for example `load the egress resource registry from master data`.

## Pull requests

- A sentence-case title.
- A description with a Summary, a Changes section grouped by area, and a closing line on user-visible impact.
- State compatibility explicitly: config, exit codes, report output, the dataset and the API v2 definition copy.
- Wait for PR Checks to pass, then squash-merge (except `task/sync_main`, see Branches).

## Never commit

- Real values in `config.py`.
- Anything in `config/` except the `*_example.json` templates.
- `*.tfstate` files.
- `datasets/` and `reports/`.
- `.env` files, or credentials of any kind.

## Compatibility rules

- The CLI must keep working fully offline, without an account.
- The public contract is the `cloudexit` command, `python -m cloudexit` and `cloudexit.cli:main`. Everything else under `cloudexit.*` is internal and may move in any 2.x release. `python3 main.py` works from a checkout through a small root script that is not installed.
- `docs/api-v2/` is a vendored copy of the API v2 definition. Never edit it here; the version check test enforces this.
- The dataset is read only by column name. The CLI must tolerate extra tables and columns.

## Security

Never log API keys, cloud credentials, the `Authorization` header or full request and response payloads. Never add telemetry.
Loading
Loading