A Python toolkit for DHIS2 — pure client library, CLI, MCP server, Playwright browser automation, and a shared plugin runtime, all in one uv workspace. Targets DHIS2 v41, v42, and v43.
The repo lives at winterop-com/dhis2w-utils; PyPI ships the six publishable members under the dhis2w-* prefix. Not affiliated with DHIS2.
Learning path · step 1 of 8 — You are here. Quick install + profile + first CLI / Python call below. Next: the contributor walkthrough for the local docker stack, or jump to a surface-specific tutorial — CLI, Python, MCP.
DHIS2 already has a lightweight, official Python client that returns plain JSON dictionaries — ideal when you want a thin wrapper and a few lines in a notebook. dhis2w is built for a different need: a typed, multi-surface toolkit you can depend on across instances and versions.
- Typed, not stringly-typed. Every response is a Pydantic model generated from DHIS2's own OpenAPI spec, so your editor autocompletes fields and the type checker catches a misspelled key before you run. No guessing dictionary keys against the docs.
- One core, four surfaces. The same typed client powers a Python library, a
dhis2CLI, an MCP server, and Playwright browser automation — all sharing oneservice.pyper domain, so behaviour never drifts between them. - Built for AI agents. The MCP server exposes ~304 typed tools, one per CLI command, so any MCP host (Claude, Cursor) can drive a DHIS2 instance directly.
- Version-aware by design. Detects v41 / v42 / v43 on connect and binds the matching hand-written tree, so one codebase works across instances instead of branching on the wire shape yourself.
- Real auth. Basic, PAT, and OAuth2/OIDC with PKCE, behind a pluggable
AuthProviderprotocol, with a profile system for juggling multiple instances. - Production posture. Strict ruff + mypy + pyright, ~1,150 tests, an mkdocs-material site, and runnable examples for every supported version.
Reach for the official client when you want the smallest possible dependency and raw JSON. Reach for dhis2w when you want types, a CLI, agent tooling, and version coverage in one place. Note that dhis2w is third-party and pre-1.0.
| Package | PyPI | Purpose |
|---|---|---|
dhis2w-client |
uv add dhis2w-client |
Pure async httpx + pydantic DHIS2 client with pluggable auth (Basic, PAT, OAuth2/OIDC). Typed models from both /api/schemas and /api/openapi.json codegen. |
dhis2w-core |
uv add dhis2w-core |
Shared runtime: profile discovery, plugin registry, auth factory, token store, first-party plugins. |
dhis2w-cli |
uv tool install dhis2w-cli |
Typer console script dhis2. |
dhis2w-mcp |
uv tool install dhis2w-mcp |
FastMCP server dhis2w-mcp. |
dhis2w-mcp-bridge |
uv tool install dhis2w-mcp-bridge |
FastMCP server dhis2w-mcp-bridge — exposes the whole dhis2 CLI as a single dhis2_cli tool for small local models. |
dhis2w-browser |
uv add dhis2w-browser |
Playwright helpers for DHIS2 UI automation — PAT minting, Playwright-driven OIDC login + consent, dashboard / viz / map screenshot capture. Mounted under dhis2 browser when the [browser] extra is installed on dhis2w-cli. |
dhis2w-codegen |
workspace-only | Generator that emits pydantic models + StrEnums + CRUD accessors into dhis2w_client.generated.v{N}/. Two source-of-truth paths: /api/schemas for metadata resources, /api/openapi.json for instance-side shapes (tracker writes, envelopes, auth schemes). |
All six publishable packages release together (lockstep versioning); see docs/releasing.md.
The CLI command is named dhis2 but the PyPI distribution is dhis2w-cli — that's why every install command spells out the package name explicitly.
# Install once, run forever — drops `dhis2` on $PATH
uv tool install dhis2w-cli
# With Playwright UI automation (browser screenshots, OIDC login, PAT minting)
uv tool install 'dhis2w-cli[browser]'
playwright install chromium # one-time, after the install above
# Update to the latest release
uv tool upgrade dhis2w-cli
# Force a re-install (handy after PyPI publish issues / cache problems)
uv tool install --reinstall dhis2w-cli
# Check what's installed
uv tool list
# Remove
uv tool uninstall dhis2w-cliAfter uv tool install dhis2w-cli, run the CLI directly:
dhis2 --help
dhis2 --version # also: -V — shows package version + active plugin tree
dhis2 system info --url https://play.im.dhis2.org/dev-2-43 --username admin --password districtdhis2 --version surfaces which plugin tree (v41 / v42 / v43) the CLI booted with and where that came from in the resolution chain (profile.version → DHIS2_VERSION env → default v42). Helps debug "which DHIS2 major is this CLI talking to" without reading the profile by hand.
uvx is uv's "run-and-forget" runner — it fetches the package into a cache and runs the binary, with no permanent install:
# uvx <command> # works when the binary name == the package name
# uvx --from <pkg> <cmd> # required when they differ — that's our case
uvx --from dhis2w-cli dhis2 --help
uvx --from dhis2w-cli dhis2 system info --url https://play.im.dhis2.org/dev-2-43 --username admin --password district
# With the browser extra
uvx --from 'dhis2w-cli[browser]' dhis2 browser pat --url ...
# Force a cache refresh — pulls the latest published version
uvx --refresh --from dhis2w-cli dhis2 --helpuv tool install keeps the install in its own dedicated venv (separate from any project venv), so the dhis2 binary on your $PATH can't be perturbed by a uv sync somewhere else.
# Inside a uv-managed project
uv add dhis2w-clientfrom dhis2w_client import BasicAuth, Dhis2Client
async with Dhis2Client(
base_url="https://play.im.dhis2.org/dev-2-43",
auth=BasicAuth(username="admin", password="district"),
) as client:
me = await client.system.me()
print(me.username)dhis2w-client is standalone — no dependency on dhis2w-core or the profile system. PyPI users who want the typed async client + generated metadata models stop here.
dhis2w-mcp exposes ~304 typed tools (one per CLI command) over the MCP stdio transport when connected to a DHIS2 v42 instance; v43 adds a handful more for the v43-only schema fields. Connect any MCP host — Claude Desktop, Claude Code, Cursor, or anything that speaks stdio MCP.
The PyPI distribution name is the binary name here (dhis2w-mcp), so the --from dance isn't needed:
# Install once — drops `dhis2w-mcp` on $PATH
uv tool install dhis2w-mcp
# Update later
uv tool upgrade dhis2w-mcp
# Or run on demand without installing
uvx dhis2w-mcp
# Force a fresh fetch (after a new PyPI release)
uvx --refresh dhis2w-mcpClaude Desktop — edit ~/Library/Application Support/Claude/claude_desktop_config.json (macOS) or %APPDATA%\Claude\claude_desktop_config.json (Windows):
{
"mcpServers": {
"dhis2": {
"command": "uvx",
"args": ["dhis2w-mcp"],
"env": {
"DHIS2_URL": "https://play.im.dhis2.org/dev-2-43",
"DHIS2_USERNAME": "admin",
"DHIS2_PASSWORD": "district"
}
}
}
}Restart Claude Desktop. PAT auth works the same way — replace the username/password pair with "DHIS2_PAT": "d2p_...".
Claude Code — register from any shell:
claude mcp add dhis2 -s user \
-e DHIS2_URL=https://play.im.dhis2.org/dev-2-43 \
-e DHIS2_PAT=d2p_... \
-- uvx dhis2w-mcp-s user makes the server available across every project. Tools land in-session as mcp__dhis2__system_whoami, mcp__dhis2__metadata_data_element_list, etc.
Cursor — edit ~/.cursor/mcp.json with the same JSON shape as Claude Desktop and reload.
The full per-client setup, profile-based auth (.dhis2/profiles.toml for OAuth2 / OIDC), tool-naming convention, and troubleshooting are in packages/dhis2w-mcp/README.md.
For a small model running on-box (LM Studio / Ollama / llama.cpp) against data that can't leave the machine, dhis2w-mcp-bridge exposes the whole CLI as a single tool, dhis2_cli, that the model drives by progressive discovery — ~one tool schema instead of ~304. (Why one tool, not many: Bridge design. Use the full dhis2w-mcp server above for capable cloud models.)
uv tool install dhis2w-mcp-bridge # or run on demand: uvx dhis2w-mcp-bridgeLM Studio (native MCP client) — ~/.lmstudio/mcp.json:
{
"mcpServers": {
"dhis2": {
"command": "dhis2w-mcp-bridge",
"env": { "DHIS2_PROFILE": "local_basic", "DHIS2_MCP_READONLY": "1" }
}
}
}The model then drives the CLI like a terminal, pulling help on demand:
dhis2_cli(["--help"]) # discover command groups
dhis2_cli(["metadata", "list", "dataElements", "--count"]) # {"resource":"dataElements","total":1037}
dhis2_cli(["schema", "dataElement"]) # the type's fields (+ enum values)
--json is injected automatically; DHIS2_MCP_READONLY=1 refuses writes (fail-closed). Full usage + read-only details: the bridge guide.
The dhis2w-cli and dhis2w-mcp packages share a profile system that walks DHIS2_PROFILE env → ./.dhis2/profiles.toml → ~/.config/dhis2/profiles.toml:
# One-shot bootstrap: prompts for URL + auth, saves a profile
dhis2 profile bootstrap mywork
# List what's known
dhis2 profile list
# Switch the default
dhis2 profile default myworkfrom dhis2w_core.client_context import open_client
from dhis2w_core.profile import profile_from_env
async with open_client(profile_from_env()) as client:
me = await client.system.me()
print(me.username)PyPI consumers who want the library without the profile layer can construct Dhis2Client(url, auth=BasicAuth(...)) directly — see examples/v42/client/library_only_auth.py.
Eighteen top-level domains; every plugin shares a service.py between the CLI and MCP sides so one typed call answers both surfaces.
| Command | What it covers |
|---|---|
dhis2 profile |
Manage DHIS2 profiles (Basic / PAT / OAuth2) + the default precedence chain |
dhis2 system |
/api/system/info, /api/me, minted UIDs |
dhis2 metadata |
List / get / export / import any metadata resource, with DHIS2's full filter + fields selector |
dhis2 data |
Aggregate data values + tracker reads + pushes |
dhis2 analytics |
Aggregated, event, enrollment, outlier-detection, and tracked-entity analytics + table rebuild |
dhis2 user |
List / get / me / invite / reinvite / reset-password |
dhis2 user-group / dhis2 user-role |
Membership + authority administration |
dhis2 route |
Integration routes (/api/routes) — register, run, inspect |
dhis2 maintenance |
Background tasks, cache clear, data-integrity, soft-delete cleanup, validation-rule runs, predictor runs, analytics-table refresh |
dhis2 files |
/api/documents + /api/fileResources — upload / download / list binary attachments |
dhis2 messaging |
/api/messageConversations — send, reply, list, mark read/unread |
dhis2 apps |
/api/apps + /api/appHub — install / uninstall / update installed apps, browse the App Hub catalog, point DHIS2 at a custom App Hub |
dhis2 doctor |
One-command preflight — ~100 metadata-health + integrity checks against a live instance |
dhis2 browser |
Playwright-driven UI automation (PAT minting, dashboard / viz / map screenshot capture, automated OIDC login) — only registers when the [browser] extra is installed |
dhis2 dev |
Codegen, UID gen, PAT / OAuth2 seed helpers, branding (dev customize), sample data |
Full per-command reference: dhis2 --help (or uvx --from dhis2w-cli dhis2 --help — the package is dhis2w-cli but the binary is dhis2, so uvx --from is required).
git clone git@github.com:winterop-com/dhis2w-utils.git
cd dhis2w-utils
make install # sync workspace deps (uv sync --all-packages --all-extras)
make lint # ruff + mypy + pyright
make test # pytest across all members
make docs-serve # local mkdocs-material
# Bring up a fully-seeded DHIS2 v43 on :8080 (Flyway-bootstraps; v42 still has a seeded e2e dump)
make dhis2-run
# Refresh codegen against the public play instances (no docker needed)
make dhis2-codegen-playSee docs/guides/connecting-to-dhis2.md for the full end-to-end walkthrough covering Basic, PAT, and OAuth2/OIDC — including the dhis.conf keys the OAuth2 path needs on the DHIS2 server, manual OAuth2 client registration without the seed script, the openId user field, and a troubleshooting matrix of every failure mode.
- Architecture + plugin walkthroughs:
docs/architecture/ - API reference (mkdocstrings-rendered):
docs/api/ - Releasing:
docs/releasing.md - Roadmap:
docs/roadmap.md - Upstream DHIS2 quirks we've tripped over:
BUGS.md - Runnable examples: three trees (
examples/v41/,examples/v42/,examples/v43/), each withcli/,client/, andmcp/subfolders.examples/v43/client/carries seven divergence-focused examples that exist only on v43 (removed_resources.py,section_user_removed.py,category_combo_coc_regen.py,event_visualization_fix_headers.py, etc.) — seedocs/architecture/schema-diff-v41-v42-v43.mdfor the underlying schema drift.examples/v41/client/carries v41-only quirks (oauth2_cid_field.py,grid_rows_wire_shape.py,apps_display_name.py).
Hard requirements, conventions, and the plugin / auth / workspace model are documented in CLAUDE.md and the docs/ site.