Workspace layout¶
The repo is a uv workspace with a virtual root (the root pyproject.toml has no [project], only [tool.uv.workspace]). Every shippable unit of code is its own member under packages/.
Why a workspace instead of one package¶
Three reasons:
dhis2w-clienthas to be publishable on its own. A single-package layout would force PyPI users of the client to pull in Typer, FastMCP, Playwright — none of which they need. A workspace lets us ship the client lean.- CLI and MCP shouldn't be the same install. A server running
dhis2w-mcpin a Docker image doesn't need the CLI's Typer tree. A developer runningdhis2locally doesn't need the MCP stdio loop. Separate members, separate wheels. - New surfaces land cleanly. A future FastAPI web UI or a TUI is a new folder, not a conditional import inside an existing package.
Layout¶
dhis2-utils/
├── pyproject.toml # virtual workspace root + shared tool config
├── uv.lock # single workspace-wide lock
├── Makefile # drives install/lint/test/docs/build/publish
├── mkdocs.yml # docs config (claude theme, left-side nav)
├── CLAUDE.md # non-negotiable project rules
├── docs/ # this site's source
├── site/ # mkdocs output (gitignored)
├── examples/
└── packages/
├── dhis2w-client/ # pure httpx + pydantic lib, PyPI-publishable
├── dhis2w-core/ # profile system + plugin runtime + first-party plugins
├── dhis2w-cli/ # Typer console script
├── dhis2w-mcp/ # FastMCP server
├── dhis2w-browser/ # Playwright helpers
└── dhis2w-codegen/ # generator — registers `dhis2 codegen` subcommand
Configuration split¶
All lint/type/test tooling (ruff, mypy, pyright, pytest, coverage) is configured once at the workspace root. Members inherit these settings automatically — no per-member ruff.toml or duplicated mypy stanzas.
Each member's pyproject.toml has just:
[project]— name, version, description, Python floor, dependencies[project.scripts]— console entrypoints (onlydhis2w-clianddhis2w-mcp)[project.entry-points."dhis2.plugins"]— plugin registration (fordhis2w-codegenand future plugin packages)[build-system]—uv_buildbackend
Build + publish¶
make build produces wheels for all members. make publish-client PUBLISH=1 pushes only dhis2w-client to PyPI — the other members stay private.
Open questions¶
- Do we ever publish
dhis2w-codegento PyPI? Probably, once it stabilises. Users of the client library outside this repo would still need a way to generate versioned models for their own instance. - Docs per-member or one site? Currently one site. If per-member doc surfaces grow significantly, we may split to one mkdocs config per member stitched together, but starting unified is simpler.