This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
This repo ships one artifact: dk-installer.py — a single-file, stdlib-only Python script that end users download and run to install/upgrade/start/demo the open-source DataKitchen products (TestGen and DataOps Observability) locally. TestGen supports both Docker Compose and pip-via-uv install modes; Observability is Docker-only. On Windows it is also packaged as dk-installer.exe via PyInstaller (see .github/workflows/release_exe.yml).
The demo/ directory is a separate deliverable: it is built into the datakitchen/data-observability-demo Docker image that dk-installer.py pulls at runtime to generate demo data. It is not imported by the installer.
pip install .[dev,test] # install ruff + pytest (project has no runtime deps)
ruff check --show-fixes # lint (CI-enforced)
ruff format --check --diff # format check (CI-enforced)
ruff format # apply formatting
pytest # run full test suite
pytest tests/test_tg_install.py # single file
pytest tests/test_action.py::test_name # single test
pytest -m unit # only unit-marked tests
pytest -m integration # only integration-marked tests
pytest --cov --cov-report=term-missing # with coverage (matches CI)
python3 dk-installer.py --help # see installer CLI
python3 dk-installer.py tg install # run an action locally during devBuilding the Windows .exe happens automatically on every push to main (release_exe.yml → PyInstaller → GitHub Release tagged latest). For local builds on Windows, see docs/build_windows_installer.md.
The installer is a ~2300-line single file intentionally using only the Python stdlib — users run it without installing any packages. Do not introduce third-party runtime dependencies.
Core abstractions (all in dk-installer.py):
Installer— top-level argparse wrapper.get_installer_instance()at the bottom of the file registers the two products (obs,tg) and their actions. Each product sets compose-file defaults (compose_file_name,compose_project_name) that flow into actions via argparseset_defaults.Action— base class for one CLI subcommand (e.g.,tg install). Owns session-scoped concerns: creates a timestamped log folder under.dk-installer/(or%LOCALAPPDATA%/DataKitchenApps/on Windows), configures logging, zips logs on exit, wraps execution inAnalyticsWrapper, enforcesrequirements(list ofRequirementobjects that shell out to checkdocker,docker compose, etc.), and providesrun_cmd/run_cmd_retries— always use these rather than rawsubprocessso output is captured per-command into the session zip.MultiStepAction—Actionsubclass that declares asteps: list[type[Step]]. EachStephaspre_execute(run for all steps before any executes — validation phase) thenexecute(the actual work). On any step failure, remaining steps are skipped andon_action_failruns in reverse order; on success,on_action_successruns in reverse order. Most install/upgrade actions areMultiStepActions — when adding a new install phase, write a newStepclass and add it to the list.Step— unit of work inside aMultiStepAction. Steps share state viaaction.ctx(a dict on the parent action). RaisingSkipStepfromexecutemarks it SKIPPED; raising any other exception marks it FAILED and aborts the action ifrequired = True.ComposeActionMixin/ComposeDeleteAction/ComposePullImagesStep/ComposeStartStep/CreateComposeFileStepBase— shared building blocks for both products.Obs*andTestGen*classes specialize these.AnalyticsWrapper— sends anonymous Mixpanel events for each action (disabled with--no-analyticsorDK_INSTALLER_ANALYTICS=no). Instance ID is persisted to.dk-installer/instance.txt. Don't log PII here.Console(globalCONSOLE) — all user-facing output goes through this; don't use bareprintfor user messages (the menu code andcollect_user_inputare the exceptions).Menu/show_menu— only used when the frozen Windows.exeis launched with no arguments (double-click). Not part of the CLI flow on Unix.
The action registry in get_installer_instance() is the authoritative list of user-facing commands — to add a new command, add an Action subclass there.
TestGen has two install modes: docker (Compose) and pip (uv-managed venv with embedded Postgres). Mode is recorded at install time in a JSON marker file (dk-tg-install.json) so tg upgrade / tg delete / tg start / tg run-demo / tg delete-demo know which path to take.
The five Testgen*Action classes that span both modes follow a unified pattern:
_per_invocation_attrsincludes_resolved_mode(andsteps/intro_textfor theMultiStepAction-based ones) so menu re-runs start clean.check_requirementsresolves mode once via_resolve_install_mode, then callssuper().check_requirements._resolve_install_modereads the marker (or runs auto-detect forinstall), setsself._resolved_mode, optionally recordsanalytics["install_mode"]. Install/upgrade/start/run-demo abort when no install exists; delete and delete-demo are idempotent (return rather than raise).get_requirementsreadsself._resolved_mode— Docker reqs only when in Docker mode.executebranches onself._resolved_mode. ForMultiStepActionsubclasses,self.stepsis also swapped at resolution time using class-levelpip_steps/docker_steps.
The pip path bootstraps a pinned uv from the astral-sh GitHub release if one isn't already on PATH (see "Bumping uv" below), then runs uv tool install to put dataops-testgen in a managed venv. After install, the app is auto-started via start_testgen_app (foreground until Ctrl+C); tg start brings it up again later. TestGen reads its config from ~/.testgen/config.env — port, SSL, and TESTGEN_LOG_FILE_PATH are all written there at standalone-setup time.
- Unix: installer writes the compose file, credentials file, and
demo-config.jsonnext todk-installer.py; logs go to./.dk-installer/<action>-<timestamp>.zip. - Windows: data and logs go to
%LOCALAPPDATA%/DataKitchenApps/.
The demo/ tree is built into a separate image (datakitchen/data-observability-demo:latest) via demo/deploy/build-image. DemoContainerAction in dk-installer.py pulls this image and mounts demo-config.json into it. Changes to demo/*.py don't affect the installer until that image is rebuilt and pushed.
The pip install path bootstraps a known version of uv from the astral-sh GitHub release. Two top-level constants govern this:
UV_VERSION— the pinned version (e.g.,"0.11.7").UV_ASSETS— a(platform.system(), platform.machine()) → (asset_name, sha256)map. Six entries: Linux x86_64/aarch64, Darwin x86_64/arm64, Windows AMD64/ARM64.
To bump:
- Update
UV_VERSION. - Pull the matching
dist-manifest.jsonfromhttps://github.com/astral-sh/uv/releases/download/<version>/dist-manifest.jsonand refresh the SHA256 for each of the 6 assets inUV_ASSETS. Each release also publishes a<asset>.sha256file you cancurldirectly if you'd rather pin one at a time. - Sanity-check:
pytest tests/test_uv_bootstrap.py. The bootstrap step exercises hash verification and the asset-not-supported path.
Do not skip the hash refresh — TLS verification is intentionally relaxed for the GitHub download (corp-proxy support), and the SHA256 pin is the security guarantee.
tests/installer.pyis a symlink to../dk-installer.py— tests import installer internals asfrom tests.installer import .... Don't replace this with a copy.- Heavy use of
unittest.mock.patchto stubsubprocess/start_cmd/run_cmd. The key fixtures live intests/conftest.py—action_clspatches class-level attributes onActionso tests can instantiate actions without a real session folder, andargs_mockprovides a fully-populatedargparse.Namespace. - Tests are marked
@pytest.mark.unitor@pytest.mark.integration. CI runs everything; use the markers locally to scope a run.
- Line length 120, double quotes, ruff-enforced (
pyproject.tomlrestricts ruff'sincludetodk-installer.pyonly — thedemo/andtests/trees are deliberately not linted by this project's ruff config). - Pre-commit hooks run ruff on commit (
.pre-commit-config.yaml). Install once withpre-commit install. - Target Python is 3.9 (CI uses 3.9); avoid 3.10+ syntax like
matchstatements orX | Ytype unions in new code — the file usestyping.Union/typing.Optionaldeliberately for this reason.
.github/workflows/pull_request.yml runs ruff + pytest (with coverage comment) on every PR against main. release_exe.yml publishes the Windows .exe on every push to main by force-moving the latest tag and recreating the release — keep this in mind before merging, since each merge replaces the public download.