enkindler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- enkindler-0.1.0/.github/workflows/ci.yml +41 -0
- enkindler-0.1.0/.gitignore +42 -0
- enkindler-0.1.0/CLAUDE.md +142 -0
- enkindler-0.1.0/Makefile +108 -0
- enkindler-0.1.0/PKG-INFO +94 -0
- enkindler-0.1.0/README.md +57 -0
- enkindler-0.1.0/pyproject.toml +67 -0
- enkindler-0.1.0/src/enkindler_cli/__init__.py +3 -0
- enkindler-0.1.0/src/enkindler_cli/client.py +114 -0
- enkindler-0.1.0/src/enkindler_cli/commands/__init__.py +0 -0
- enkindler-0.1.0/src/enkindler_cli/commands/auth.py +115 -0
- enkindler-0.1.0/src/enkindler_cli/commands/config.py +172 -0
- enkindler-0.1.0/src/enkindler_cli/commands/deployments.py +86 -0
- enkindler-0.1.0/src/enkindler_cli/commands/users.py +36 -0
- enkindler-0.1.0/src/enkindler_cli/config.py +186 -0
- enkindler-0.1.0/src/enkindler_cli/main.py +63 -0
- enkindler-0.1.0/tests/__init__.py +0 -0
- enkindler-0.1.0/tests/test_smoke.py +89 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
name: ${{ matrix.os }} / Python ${{ matrix.python-version }}
|
|
11
|
+
runs-on: ${{ matrix.os }}
|
|
12
|
+
strategy:
|
|
13
|
+
fail-fast: false
|
|
14
|
+
matrix:
|
|
15
|
+
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
17
|
+
steps:
|
|
18
|
+
- uses: actions/checkout@v4
|
|
19
|
+
|
|
20
|
+
- uses: actions/setup-python@v5
|
|
21
|
+
with:
|
|
22
|
+
python-version: ${{ matrix.python-version }}
|
|
23
|
+
cache: pip
|
|
24
|
+
|
|
25
|
+
# Windows runners ship a stub `make`; install GNU make via choco.
|
|
26
|
+
- name: Install GNU make (Windows)
|
|
27
|
+
if: runner.os == 'Windows'
|
|
28
|
+
run: choco install make -y --no-progress
|
|
29
|
+
|
|
30
|
+
- name: Install
|
|
31
|
+
run: make install
|
|
32
|
+
# The Makefile wraps venv setup so it's identical across OSes.
|
|
33
|
+
|
|
34
|
+
- name: Lint
|
|
35
|
+
run: make lint
|
|
36
|
+
|
|
37
|
+
- name: Test
|
|
38
|
+
run: make test
|
|
39
|
+
|
|
40
|
+
- name: Smoke
|
|
41
|
+
run: make smoke
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
.venv/
|
|
8
|
+
venv/
|
|
9
|
+
env/
|
|
10
|
+
.eggs/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
*.egg
|
|
13
|
+
build/
|
|
14
|
+
dist/
|
|
15
|
+
wheels/
|
|
16
|
+
.installed.cfg
|
|
17
|
+
|
|
18
|
+
# Generated SDK (regenerated from openapi spec at build time; never commit)
|
|
19
|
+
src/enkindler_cli/_generated/
|
|
20
|
+
|
|
21
|
+
# Test/coverage
|
|
22
|
+
.pytest_cache/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
htmlcov/
|
|
26
|
+
.tox/
|
|
27
|
+
|
|
28
|
+
# Linters
|
|
29
|
+
.ruff_cache/
|
|
30
|
+
.mypy_cache/
|
|
31
|
+
|
|
32
|
+
# IDE
|
|
33
|
+
.idea/
|
|
34
|
+
.vscode/
|
|
35
|
+
*.swp
|
|
36
|
+
|
|
37
|
+
# OS
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
40
|
+
|
|
41
|
+
# Local config that might be created during dev runs
|
|
42
|
+
*.local.toml
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Operating manual for Claude Code (and human contributors) working in this repo.
|
|
4
|
+
|
|
5
|
+
## Project Overview
|
|
6
|
+
|
|
7
|
+
`enkindler-cli` is the Python command-line client for the [Enkindler](https://enkindler.enkindl.com) AI operations dashboard. Distributed on PyPI as `pip install enkindler`. Patterned after `az` and `databricks`.
|
|
8
|
+
|
|
9
|
+
The CLI is **also exposed as a tool to AI agents**, so command behaviour, output, and error messages must be unambiguous and machine-parseable. When in doubt, optimize for "an LLM should be able to call this without trial and error."
|
|
10
|
+
|
|
11
|
+
The companion backend repo is at `../enkindler/` (sibling). The backend exposes an OpenAPI 3 spec at `/api/openapi.json`; the CLI consumes that spec to generate a typed SDK and surface commands.
|
|
12
|
+
|
|
13
|
+
## Repo layout
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
enkindler-cli/
|
|
17
|
+
├── Makefile # Source of truth for all dev/build/release commands
|
|
18
|
+
├── pyproject.toml # PEP 621 metadata; deps; ruff/pytest config
|
|
19
|
+
├── README.md # End-user docs (install, basic usage)
|
|
20
|
+
├── CLAUDE.md # ← this file
|
|
21
|
+
├── docs/ # Long-form procedures (release, sdk-sync, etc.)
|
|
22
|
+
├── .github/workflows/ci.yml # OS × Python matrix
|
|
23
|
+
├── src/enkindler_cli/
|
|
24
|
+
│ ├── __init__.py # __version__
|
|
25
|
+
│ ├── main.py # Typer app + global error handler (_entry)
|
|
26
|
+
│ ├── client.py # httpx wrapper + AuthError/APIError
|
|
27
|
+
│ ├── config.py # platformdirs-based TOML config
|
|
28
|
+
│ ├── commands/
|
|
29
|
+
│ │ ├── auth.py # login (device-code), logout
|
|
30
|
+
│ │ ├── users.py # whoami
|
|
31
|
+
│ │ └── deployments.py # list, get, ...
|
|
32
|
+
│ └── _generated/ # ← gitignored; created by `make regen-sdk`
|
|
33
|
+
└── tests/
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Always use `make`
|
|
37
|
+
|
|
38
|
+
Every common task has a make target. Use them. Never invoke `pytest` / `ruff` / `python -m build` directly — CI uses make, so should you. This is the only way command lines stay in sync across humans, CI, and Claude.
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
make help # list everything
|
|
42
|
+
make install # set up venv + install with [dev] extras
|
|
43
|
+
make test # run pytest
|
|
44
|
+
make lint # ruff check (no fix)
|
|
45
|
+
make format # ruff fix + format
|
|
46
|
+
make smoke # `enkindler --version` + `--help` smoke check
|
|
47
|
+
make build # sdist + wheel into ./dist
|
|
48
|
+
make regen-sdk # pull /api/openapi.json from backend, regenerate _generated/
|
|
49
|
+
make publish # twine upload to PyPI (needs TWINE_PASSWORD)
|
|
50
|
+
make clean # rm caches and build artifacts
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
If you need a new shell command frequently, add it as a make target — don't put it in CLAUDE.md prose.
|
|
54
|
+
|
|
55
|
+
## Conventions
|
|
56
|
+
|
|
57
|
+
### Commands
|
|
58
|
+
- One file per noun under `src/enkindler_cli/commands/` (e.g. `deployments.py`, `helm.py`, `integrations.py`).
|
|
59
|
+
- A noun with subcommands (verbs) uses a Typer sub-app: see `commands/deployments.py` — exports `app = typer.Typer()` and uses `@app.command("list")`.
|
|
60
|
+
- A noun with one verb (`whoami`, `login`) is a plain function: see `commands/users.py` — register at the top level in `main.py`.
|
|
61
|
+
- All commands accept `--api-url` (override env / config). Resolution: flag > `ENKINDLER_API_URL` > config file > default. Use `effective_api_url()` from `config.py`.
|
|
62
|
+
|
|
63
|
+
### Errors
|
|
64
|
+
- Never `raise` raw `httpx.HTTPStatusError`. Use `Client` from `client.py` — it converts non-2xx to `AuthError` (401/403) or `APIError` (other). The top-level `_entry()` in `main.py` formats both as friendly red messages with a recovery hint.
|
|
65
|
+
- For business-logic exits, `raise typer.Exit(code=N)` — never `sys.exit()`.
|
|
66
|
+
|
|
67
|
+
### Output
|
|
68
|
+
- Human output → `rich.Console()` to stdout.
|
|
69
|
+
- Errors → `rich.Console(stderr=True)`.
|
|
70
|
+
- Tables are the default for list commands. Use `rich.table.Table`.
|
|
71
|
+
- Don't print debug info to stdout — that breaks `--output json` (when we add it).
|
|
72
|
+
|
|
73
|
+
### HTTP
|
|
74
|
+
- All HTTP via `Client` from `client.py`. Always use it as a context manager.
|
|
75
|
+
- One `Client` per command invocation. Don't share connection pools across commands.
|
|
76
|
+
|
|
77
|
+
### Config
|
|
78
|
+
- Use `platformdirs` for paths. Never hardcode `~/.enkindler` — Windows uses `%APPDATA%`.
|
|
79
|
+
- TOML, not JSON or YAML.
|
|
80
|
+
- Token cached in `config.toml` with 0o600 perms on POSIX. Don't put tokens in env vars or CLI args; the only sanctioned write path is `enkindler login` → `Config.save()`.
|
|
81
|
+
|
|
82
|
+
### Cross-platform
|
|
83
|
+
We support macOS, Linux, and Windows on Python 3.10–3.13. CI runs the full matrix. Avoid:
|
|
84
|
+
- `os.path.join('/etc/...')`, hardcoded slashes — use `pathlib`
|
|
85
|
+
- Shelling out to Unix-only tools (`curl`, `grep`, `sed`)
|
|
86
|
+
- `subprocess` with `shell=True` — pass an arg list
|
|
87
|
+
- Symlinks
|
|
88
|
+
- ANSI escape codes in non-TTY output (Rich handles this if you let it)
|
|
89
|
+
|
|
90
|
+
## Common Tasks
|
|
91
|
+
|
|
92
|
+
### Add a new command
|
|
93
|
+
1. Create or edit a file under `src/enkindler_cli/commands/`.
|
|
94
|
+
2. If it's a new noun, add a sub-`Typer` and register it in `main.py` via `app.add_typer(... name="noun")`.
|
|
95
|
+
3. Add a smoke test in `tests/test_smoke.py` that the new command appears in `--help`.
|
|
96
|
+
4. Run `make lint test` before committing.
|
|
97
|
+
|
|
98
|
+
### Add a new endpoint to call
|
|
99
|
+
Once the backend annotates a new route (see backend `CLAUDE.md` → "Annotating routes for OpenAPI/CLI"):
|
|
100
|
+
1. Run `make regen-sdk` (or `make regen-sdk OPENAPI_URL=http://localhost:5179/api/openapi.json` while iterating locally).
|
|
101
|
+
2. Use the generated client from `_generated/` in your command. Until the SDK lands, hand-call via `Client.get` / `.post`.
|
|
102
|
+
|
|
103
|
+
### Cut a release
|
|
104
|
+
See `docs/release.md` (TBD — write when we cut v0.1.0). Short version:
|
|
105
|
+
1. Bump `__version__` in `src/enkindler_cli/__init__.py` AND `version` in `pyproject.toml`.
|
|
106
|
+
2. `make clean build`
|
|
107
|
+
3. `make publish-test` → install from TestPyPI in a fresh venv, smoke-test
|
|
108
|
+
4. `make publish`
|
|
109
|
+
5. `git tag vX.Y.Z && git push --tags`
|
|
110
|
+
|
|
111
|
+
### Backend out of sync with CLI
|
|
112
|
+
If `make regen-sdk` fails or the generated SDK doesn't compile, the backend OpenAPI spec is malformed. Fix it in `../enkindler/backend/src/server.js` (the JSDoc `@openapi` block above the offending route) — see backend `CLAUDE.md` for the annotation pattern.
|
|
113
|
+
|
|
114
|
+
## Testing the CLI against a real backend
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
make install
|
|
118
|
+
make smoke # offline smoke
|
|
119
|
+
.venv/bin/enkindler login # device-code flow against prod backend
|
|
120
|
+
.venv/bin/enkindler whoami
|
|
121
|
+
.venv/bin/enkindler deployment list
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Override the backend for local dev:
|
|
125
|
+
```bash
|
|
126
|
+
ENKINDLER_API_URL=http://localhost:5179 .venv/bin/enkindler login
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## What NOT to do
|
|
130
|
+
|
|
131
|
+
- Don't add a `requirements.txt`. Deps live in `pyproject.toml`.
|
|
132
|
+
- Don't pin sub-dependencies in `pyproject.toml` — only direct deps.
|
|
133
|
+
- Don't commit `src/enkindler_cli/_generated/` — gitignored, regenerated on demand.
|
|
134
|
+
- Don't add Click decorators directly. Use Typer (which wraps Click) — it gives us free `--help` from type hints.
|
|
135
|
+
- Don't break Python 3.10. We support it until end-of-life.
|
|
136
|
+
- Don't import optional deps at the top of a module — gate behind `if TYPE_CHECKING:` or import inside the function. Keeps `enkindler --help` snappy.
|
|
137
|
+
|
|
138
|
+
## Pointers to backend repo
|
|
139
|
+
|
|
140
|
+
- Backend endpoints, auth model, RBAC, deployment infrastructure → `../enkindler/CLAUDE.md`
|
|
141
|
+
- Backend OpenAPI annotation pattern → `../enkindler/CLAUDE.md` → "Annotating routes for OpenAPI/CLI"
|
|
142
|
+
- Device-code auth implementation → `../enkindler/backend/src/auth/deviceCode.js`
|
enkindler-0.1.0/Makefile
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# enkindler-cli — canonical command set.
|
|
2
|
+
#
|
|
3
|
+
# Both humans and CI use these targets so there is one source of truth.
|
|
4
|
+
# `make help` lists everything.
|
|
5
|
+
|
|
6
|
+
PYTHON ?= python3
|
|
7
|
+
VENV ?= .venv
|
|
8
|
+
|
|
9
|
+
# venv layout differs on Windows (Scripts/) vs POSIX (bin/).
|
|
10
|
+
ifeq ($(OS),Windows_NT)
|
|
11
|
+
VENV_BIN := $(VENV)/Scripts
|
|
12
|
+
PYTHON := python
|
|
13
|
+
else
|
|
14
|
+
VENV_BIN := $(VENV)/bin
|
|
15
|
+
endif
|
|
16
|
+
PIP := $(VENV_BIN)/pip
|
|
17
|
+
PY := $(VENV_BIN)/python
|
|
18
|
+
ENK := $(VENV_BIN)/enkindler
|
|
19
|
+
|
|
20
|
+
# Backend OpenAPI spec — used by `make regen-sdk`. Override with
|
|
21
|
+
# `make regen-sdk OPENAPI_URL=http://localhost:5179/api/openapi.json`
|
|
22
|
+
# when iterating on the backend locally.
|
|
23
|
+
OPENAPI_URL ?= https://enkindler.enkindl.com/api/openapi.json
|
|
24
|
+
|
|
25
|
+
# Where the auto-generated openapi-python-client output lives.
|
|
26
|
+
# Gitignored — regenerated from the backend spec on demand.
|
|
27
|
+
SDK_DIR := src/enkindler_cli/_generated
|
|
28
|
+
|
|
29
|
+
.DEFAULT_GOAL := help
|
|
30
|
+
.PHONY: help setup install dev test lint format typecheck build clean publish-test publish regen-sdk smoke version
|
|
31
|
+
|
|
32
|
+
help: ## Show this help.
|
|
33
|
+
@awk 'BEGIN { FS = ":.*?## " } /^[a-zA-Z0-9_-]+:.*?## / { printf " \033[36m%-16s\033[0m %s\n", $$1, $$2 }' $(MAKEFILE_LIST)
|
|
34
|
+
|
|
35
|
+
# ─── Local dev setup ─────────────────────────────────────────────────────────
|
|
36
|
+
|
|
37
|
+
$(VENV_BIN)/python:
|
|
38
|
+
$(PYTHON) -m venv $(VENV)
|
|
39
|
+
$(PIP) install --upgrade pip
|
|
40
|
+
|
|
41
|
+
setup: $(VENV_BIN)/python ## Create virtualenv and install runtime deps only.
|
|
42
|
+
$(PIP) install -e .
|
|
43
|
+
|
|
44
|
+
install: $(VENV_BIN)/python ## Alias for `setup` + dev extras.
|
|
45
|
+
$(PIP) install -e '.[dev]'
|
|
46
|
+
|
|
47
|
+
dev: install ## Same as `install` — for muscle memory.
|
|
48
|
+
|
|
49
|
+
# ─── Quality gates ───────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
test: install ## Run the test suite.
|
|
52
|
+
$(VENV_BIN)/pytest
|
|
53
|
+
|
|
54
|
+
lint: install ## Lint with ruff (no auto-fix).
|
|
55
|
+
$(VENV_BIN)/ruff check .
|
|
56
|
+
|
|
57
|
+
format: install ## Auto-format and apply safe ruff fixes.
|
|
58
|
+
$(VENV_BIN)/ruff check --fix .
|
|
59
|
+
$(VENV_BIN)/ruff format .
|
|
60
|
+
|
|
61
|
+
typecheck: ## (Stub) — add when we adopt a type checker.
|
|
62
|
+
@echo "No type checker configured yet. Add mypy or pyright when ready."
|
|
63
|
+
|
|
64
|
+
smoke: install ## Cheap end-to-end check that the script entrypoint works.
|
|
65
|
+
$(ENK) --version
|
|
66
|
+
$(ENK) --help > /dev/null
|
|
67
|
+
|
|
68
|
+
version: install ## Print the package version.
|
|
69
|
+
@$(ENK) --version
|
|
70
|
+
|
|
71
|
+
# ─── Build & publish ─────────────────────────────────────────────────────────
|
|
72
|
+
|
|
73
|
+
build: install ## Build sdist + wheel into ./dist/
|
|
74
|
+
$(PIP) install --quiet build
|
|
75
|
+
$(PY) -m build
|
|
76
|
+
|
|
77
|
+
publish-test: build ## Upload to TestPyPI (requires TWINE_PASSWORD env).
|
|
78
|
+
$(PIP) install --quiet twine
|
|
79
|
+
$(VENV_BIN)/twine upload --repository testpypi dist/*
|
|
80
|
+
|
|
81
|
+
publish: build ## Upload to PyPI (requires TWINE_PASSWORD env).
|
|
82
|
+
$(PIP) install --quiet twine
|
|
83
|
+
$(VENV_BIN)/twine upload dist/*
|
|
84
|
+
|
|
85
|
+
# ─── SDK regen from backend OpenAPI spec ─────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
regen-sdk: install ## Regenerate the typed SDK from the backend OpenAPI spec.
|
|
88
|
+
$(PIP) install --quiet openapi-python-client
|
|
89
|
+
rm -rf $(SDK_DIR)
|
|
90
|
+
mkdir -p $(SDK_DIR)
|
|
91
|
+
$(VENV_BIN)/openapi-python-client generate \
|
|
92
|
+
--url $(OPENAPI_URL) \
|
|
93
|
+
--output-path $(SDK_DIR)/_tmp \
|
|
94
|
+
--meta none
|
|
95
|
+
# openapi-python-client puts files in <project_name>/, flatten one level
|
|
96
|
+
mv $(SDK_DIR)/_tmp/*/* $(SDK_DIR)/
|
|
97
|
+
rm -rf $(SDK_DIR)/_tmp
|
|
98
|
+
@echo "SDK regenerated into $(SDK_DIR) from $(OPENAPI_URL)"
|
|
99
|
+
|
|
100
|
+
# ─── Housekeeping ────────────────────────────────────────────────────────────
|
|
101
|
+
|
|
102
|
+
clean: ## Remove build artifacts and caches (keeps the venv).
|
|
103
|
+
rm -rf build dist *.egg-info
|
|
104
|
+
rm -rf .pytest_cache .ruff_cache .mypy_cache
|
|
105
|
+
find . -type d -name __pycache__ -prune -exec rm -rf {} +
|
|
106
|
+
|
|
107
|
+
clean-all: clean ## Also delete the virtualenv.
|
|
108
|
+
rm -rf $(VENV)
|
enkindler-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: enkindler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Command-line interface for the Enkindler AI operations dashboard.
|
|
5
|
+
Project-URL: Homepage, https://enkindler.enkindl.com
|
|
6
|
+
Project-URL: Repository, https://bitbucket.org/enkindl/enkindler-cli
|
|
7
|
+
Project-URL: Issues, https://bitbucket.org/enkindl/enkindler-cli/issues
|
|
8
|
+
Author: Enkindler Platform
|
|
9
|
+
License: MIT
|
|
10
|
+
Keywords: aks,azure,cli,enkindler,kubernetes
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Intended Audience :: System Administrators
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: MacOS
|
|
17
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
18
|
+
Classifier: Operating System :: POSIX :: Linux
|
|
19
|
+
Classifier: Programming Language :: Python :: 3
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
24
|
+
Classifier: Topic :: System :: Systems Administration
|
|
25
|
+
Requires-Python: >=3.10
|
|
26
|
+
Requires-Dist: httpx>=0.27.0
|
|
27
|
+
Requires-Dist: platformdirs>=4.2.0
|
|
28
|
+
Requires-Dist: rich>=13.7.0
|
|
29
|
+
Requires-Dist: tomli-w>=1.0.0
|
|
30
|
+
Requires-Dist: tomli>=2.0.1; python_version < '3.11'
|
|
31
|
+
Requires-Dist: typer>=0.12.0
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest-httpx>=0.30; extra == 'dev'
|
|
34
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: ruff>=0.5.0; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# enkindler CLI
|
|
39
|
+
|
|
40
|
+
Command-line interface for the [Enkindler](https://enkindler.enkindl.com) AI operations dashboard. Manages AKS deployments, Helm platforms, integrations, and cluster resources from your terminal.
|
|
41
|
+
|
|
42
|
+
Patterned after `az` and `databricks` — designed to be ergonomic for both humans and AI agents.
|
|
43
|
+
|
|
44
|
+
## Install
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install enkindler
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Works on macOS, Linux, and Windows (Python 3.10+).
|
|
51
|
+
|
|
52
|
+
## Quick start
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
# Sign in via Microsoft device-code flow (opens browser)
|
|
56
|
+
enkindler login
|
|
57
|
+
|
|
58
|
+
# Verify the session
|
|
59
|
+
enkindler whoami
|
|
60
|
+
|
|
61
|
+
# List your deployments
|
|
62
|
+
enkindler deployment list
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Configuration
|
|
66
|
+
|
|
67
|
+
Config and the cached JWT live at:
|
|
68
|
+
|
|
69
|
+
| OS | Path |
|
|
70
|
+
|---|---|
|
|
71
|
+
| macOS | `~/Library/Application Support/enkindler/config.toml` |
|
|
72
|
+
| Linux | `~/.config/enkindler/config.toml` |
|
|
73
|
+
| Windows | `%APPDATA%\enkindler\config.toml` |
|
|
74
|
+
|
|
75
|
+
Override the backend URL with `--api-url https://...` on any command, or set `ENKINDLER_API_URL` in the environment.
|
|
76
|
+
|
|
77
|
+
## Architecture
|
|
78
|
+
|
|
79
|
+
- Backend exposes an OpenAPI spec at `/api/openapi.json` (the contract).
|
|
80
|
+
- This CLI consumes that spec to drive command behaviour.
|
|
81
|
+
- Auth uses the Azure AD device-code flow — no browser required on the same machine, no token-pasting.
|
|
82
|
+
|
|
83
|
+
## Development
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
python -m venv .venv && source .venv/bin/activate # or `.venv\Scripts\activate` on Windows
|
|
87
|
+
pip install -e '.[dev]'
|
|
88
|
+
pytest
|
|
89
|
+
ruff check .
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
## License
|
|
93
|
+
|
|
94
|
+
MIT
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# enkindler CLI
|
|
2
|
+
|
|
3
|
+
Command-line interface for the [Enkindler](https://enkindler.enkindl.com) AI operations dashboard. Manages AKS deployments, Helm platforms, integrations, and cluster resources from your terminal.
|
|
4
|
+
|
|
5
|
+
Patterned after `az` and `databricks` — designed to be ergonomic for both humans and AI agents.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install enkindler
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Works on macOS, Linux, and Windows (Python 3.10+).
|
|
14
|
+
|
|
15
|
+
## Quick start
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
# Sign in via Microsoft device-code flow (opens browser)
|
|
19
|
+
enkindler login
|
|
20
|
+
|
|
21
|
+
# Verify the session
|
|
22
|
+
enkindler whoami
|
|
23
|
+
|
|
24
|
+
# List your deployments
|
|
25
|
+
enkindler deployment list
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Configuration
|
|
29
|
+
|
|
30
|
+
Config and the cached JWT live at:
|
|
31
|
+
|
|
32
|
+
| OS | Path |
|
|
33
|
+
|---|---|
|
|
34
|
+
| macOS | `~/Library/Application Support/enkindler/config.toml` |
|
|
35
|
+
| Linux | `~/.config/enkindler/config.toml` |
|
|
36
|
+
| Windows | `%APPDATA%\enkindler\config.toml` |
|
|
37
|
+
|
|
38
|
+
Override the backend URL with `--api-url https://...` on any command, or set `ENKINDLER_API_URL` in the environment.
|
|
39
|
+
|
|
40
|
+
## Architecture
|
|
41
|
+
|
|
42
|
+
- Backend exposes an OpenAPI spec at `/api/openapi.json` (the contract).
|
|
43
|
+
- This CLI consumes that spec to drive command behaviour.
|
|
44
|
+
- Auth uses the Azure AD device-code flow — no browser required on the same machine, no token-pasting.
|
|
45
|
+
|
|
46
|
+
## Development
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python -m venv .venv && source .venv/bin/activate # or `.venv\Scripts\activate` on Windows
|
|
50
|
+
pip install -e '.[dev]'
|
|
51
|
+
pytest
|
|
52
|
+
ruff check .
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "enkindler"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Command-line interface for the Enkindler AI operations dashboard."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "Enkindler Platform" }]
|
|
13
|
+
keywords = ["enkindler", "cli", "aks", "kubernetes", "azure"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Intended Audience :: System Administrators",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Operating System :: MacOS",
|
|
21
|
+
"Operating System :: POSIX :: Linux",
|
|
22
|
+
"Operating System :: Microsoft :: Windows",
|
|
23
|
+
"Programming Language :: Python :: 3",
|
|
24
|
+
"Programming Language :: Python :: 3.10",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"Programming Language :: Python :: 3.12",
|
|
27
|
+
"Programming Language :: Python :: 3.13",
|
|
28
|
+
"Topic :: System :: Systems Administration",
|
|
29
|
+
]
|
|
30
|
+
dependencies = [
|
|
31
|
+
"typer>=0.12.0",
|
|
32
|
+
"rich>=13.7.0",
|
|
33
|
+
"httpx>=0.27.0",
|
|
34
|
+
"platformdirs>=4.2.0",
|
|
35
|
+
"tomli>=2.0.1; python_version<'3.11'",
|
|
36
|
+
"tomli-w>=1.0.0",
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
[project.optional-dependencies]
|
|
40
|
+
dev = [
|
|
41
|
+
"pytest>=8.0",
|
|
42
|
+
"pytest-httpx>=0.30",
|
|
43
|
+
"ruff>=0.5.0",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[project.scripts]
|
|
47
|
+
enkindler = "enkindler_cli.main:_entry"
|
|
48
|
+
|
|
49
|
+
[project.urls]
|
|
50
|
+
Homepage = "https://enkindler.enkindl.com"
|
|
51
|
+
Repository = "https://bitbucket.org/enkindl/enkindler-cli"
|
|
52
|
+
Issues = "https://bitbucket.org/enkindl/enkindler-cli/issues"
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/enkindler_cli"]
|
|
56
|
+
|
|
57
|
+
[tool.ruff]
|
|
58
|
+
line-length = 100
|
|
59
|
+
target-version = "py310"
|
|
60
|
+
|
|
61
|
+
[tool.ruff.lint]
|
|
62
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF"]
|
|
63
|
+
ignore = ["E501"] # line length handled by formatter
|
|
64
|
+
|
|
65
|
+
[tool.pytest.ini_options]
|
|
66
|
+
testpaths = ["tests"]
|
|
67
|
+
addopts = "-ra -q"
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Thin httpx wrapper that injects the cached JWT and raises typed errors.
|
|
2
|
+
|
|
3
|
+
Eventually most calls will go through the openapi-python-client generated SDK
|
|
4
|
+
in `_generated/`. This wrapper handles the bootstrap concerns:
|
|
5
|
+
|
|
6
|
+
* the device-code login flow (no token yet)
|
|
7
|
+
* sanity calls like /api/health and /api/instance that don't require auth
|
|
8
|
+
* a uniform AuthError that the CLI can intercept with a "run `enkindler login`"
|
|
9
|
+
hint instead of a stack trace
|
|
10
|
+
|
|
11
|
+
URL resolution is now driven by the multi-cluster Config — Client.from_cluster()
|
|
12
|
+
takes a resolved Cluster object. Per-command --api-url and ENKINDLER_API_URL
|
|
13
|
+
override the cluster's stored URL but reuse its token.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
import httpx
|
|
21
|
+
|
|
22
|
+
from .config import Cluster, Config, ConfigError, effective_url_override
|
|
23
|
+
|
|
24
|
+
USER_AGENT = "enkindler-cli/0.0.1"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class APIError(Exception):
|
|
28
|
+
"""Backend returned a non-2xx status with an error envelope."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, status: int, message: str):
|
|
31
|
+
super().__init__(f"{status}: {message}")
|
|
32
|
+
self.status = status
|
|
33
|
+
self.message = message
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class AuthError(APIError):
|
|
37
|
+
"""401/403 — token missing, invalid, or insufficient privileges."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _raise(resp: httpx.Response) -> None:
|
|
41
|
+
if resp.is_success:
|
|
42
|
+
return
|
|
43
|
+
try:
|
|
44
|
+
body: dict[str, Any] = resp.json()
|
|
45
|
+
msg = body.get("error") or body.get("message") or resp.text
|
|
46
|
+
except (ValueError, httpx.DecodingError):
|
|
47
|
+
msg = resp.text or resp.reason_phrase
|
|
48
|
+
if resp.status_code in (401, 403):
|
|
49
|
+
raise AuthError(resp.status_code, msg)
|
|
50
|
+
raise APIError(resp.status_code, msg)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class Client:
|
|
54
|
+
"""Synchronous httpx client scoped to one Enkindler backend."""
|
|
55
|
+
|
|
56
|
+
def __init__(self, base_url: str, token: str | None = None, timeout: float = 30.0):
|
|
57
|
+
headers = {"User-Agent": USER_AGENT, "Accept": "application/json"}
|
|
58
|
+
if token:
|
|
59
|
+
headers["Authorization"] = f"Bearer {token}"
|
|
60
|
+
self._http = httpx.Client(base_url=base_url, headers=headers, timeout=timeout)
|
|
61
|
+
|
|
62
|
+
@classmethod
|
|
63
|
+
def from_cluster(cls, cluster: Cluster, *, api_url_override: str | None = None) -> Client:
|
|
64
|
+
url = api_url_override or effective_url_override() or cluster.url
|
|
65
|
+
return cls(base_url=url, token=cluster.token)
|
|
66
|
+
|
|
67
|
+
@classmethod
|
|
68
|
+
def for_url(cls, url: str, *, token: str | None = None) -> Client:
|
|
69
|
+
"""Bare client for endpoints that don't have a configured cluster yet
|
|
70
|
+
(e.g. `enkindler config add` verifying a fresh URL)."""
|
|
71
|
+
return cls(base_url=url.rstrip("/"), token=token)
|
|
72
|
+
|
|
73
|
+
@classmethod
|
|
74
|
+
def from_default(cls, cluster_name: str | None = None, api_url_override: str | None = None) -> Client:
|
|
75
|
+
"""Convenience: resolve via Config.load() and the optional --cluster name.
|
|
76
|
+
Raises ConfigError if no cluster is configured / resolvable."""
|
|
77
|
+
cfg = Config.load()
|
|
78
|
+
cluster = cfg.resolve(cluster_name)
|
|
79
|
+
return cls.from_cluster(cluster, api_url_override=api_url_override)
|
|
80
|
+
|
|
81
|
+
def get(self, path: str, **kwargs: Any) -> Any:
|
|
82
|
+
r = self._http.get(path, **kwargs)
|
|
83
|
+
_raise(r)
|
|
84
|
+
return r.json()
|
|
85
|
+
|
|
86
|
+
def post(self, path: str, json: Any | None = None, **kwargs: Any) -> Any:
|
|
87
|
+
r = self._http.post(path, json=json, **kwargs)
|
|
88
|
+
_raise(r)
|
|
89
|
+
if r.status_code == 204 or not r.content:
|
|
90
|
+
return None
|
|
91
|
+
return r.json()
|
|
92
|
+
|
|
93
|
+
def patch(self, path: str, json: Any | None = None, **kwargs: Any) -> Any:
|
|
94
|
+
r = self._http.patch(path, json=json, **kwargs)
|
|
95
|
+
_raise(r)
|
|
96
|
+
return r.json()
|
|
97
|
+
|
|
98
|
+
def delete(self, path: str, **kwargs: Any) -> Any:
|
|
99
|
+
r = self._http.delete(path, **kwargs)
|
|
100
|
+
_raise(r)
|
|
101
|
+
return r.json() if r.content else None
|
|
102
|
+
|
|
103
|
+
def close(self) -> None:
|
|
104
|
+
self._http.close()
|
|
105
|
+
|
|
106
|
+
def __enter__(self) -> Client:
|
|
107
|
+
return self
|
|
108
|
+
|
|
109
|
+
def __exit__(self, *exc: object) -> None:
|
|
110
|
+
self.close()
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
# Re-export for command modules.
|
|
114
|
+
__all__ = ["APIError", "AuthError", "Client", "Cluster", "Config", "ConfigError"]
|
|
File without changes
|