Skip to main content

dhis2-client

Simple, synchronous DHIS2 Web API client — dict/JSON only, Jupyter-friendly (no context managers), clean paging, read-only users, CRUD for metadata (org units, data elements, data sets), data values & data value sets, analytics, and minimal logging by default.


Table of Contents


Why this client?

DHIS2 is one of the most widely used health information platforms worldwide, and Python is a popular choice in data science, research, and integration workflows. Bringing the two together makes it easier to build analytics, integrations, and automation around DHIS2 data.

This client provides a lightweight and simple way to work with the DHIS2 Web API:

  • Always returns plain Python dict / JSON objects — no custom models or ORM layers.
  • Handles paging cleanly, so you can iterate over large DHIS2 collections without surprises.
  • Offers convenience methods for common entities (users, organisation units, data elements, data sets, data values, analytics) while keeping full access to the raw API.
  • Keeps setup minimal — synchronous, Jupyter-friendly, and easy to configure.

Installation

Requires Python 3.10+

pip install dhis2-client

To work on the client itself, install it from a checkout with the development extras:

git clone https://github.com/dhis2/dhis2-python-client.git
cd dhis2-python-client
pip install -e .[dev]

Requirements

  • Python 3.10+
  • DHIS2 server URL and valid credentials (Basic or token)

Quickstart

from dhis2_client import DHIS2Client

client = DHIS2Client(
    base_url="http://localhost:8080",
    username="admin",
    password="district",  # Basic auth by default
)

# Iterate users (read-only)
for u in client.get_users(fields="id,username", order="username:asc"):
    print(u)

# Fetch all data elements into a list (respecting paging)
all_des = client.fetch_all("/api/dataElements", params={"fields": "id,displayName"})

Authentication & Settings

You can configure the client directly with kwargs or centrally with a ClientSettings object.

from dhis2_client import DHIS2Client
from dhis2_client.settings import ClientSettings

# Recommended: central settings
cfg = ClientSettings(
    base_url="http://localhost:8080",
    username="admin",
    password="district",
    connect_timeout=60.0,   # default connect timeout (cold-start friendly)
    timeout=30.0,           # default read/write/pool timeout
    log_level="INFO",        # default "WARNING"
    log_format="json",       # default "json"; use "text" for human-readable
    log_destination="stdout" # default "stderr"; can also be file path
)

client = DHIS2Client(settings=cfg)
info = client.get_system_info()
print(info["version"])

Kwargs override settings if both are provided:

client = DHIS2Client(settings=cfg, log_level="DEBUG")  # DEBUG takes precedence

Logging

  • Default: JSON logs at WARNING level to stderr.
  • Configurable: via ClientSettings or constructor kwargs.
  • Timeouts: connect_timeout=60s by default, timeout=30s for request read/write/pool.
# JSON (default) logs at INFO to stdout
cfg = ClientSettings(
  base_url="http://localhost:8080", 
  username="admin", 
  password="district", 
  log_level="INFO", log_destination="stdout")
client = DHIS2Client(settings=cfg)

# Human-readable text logs
client = DHIS2Client(
  "http://localhost:8080",
  username="admin", 
  password="district",
  log_level="INFO", log_format="text")

# File logging
client = DHIS2Client("http://localhost:8080",
                     username="admin", password="district",
                     log_level="DEBUG", log_destination="/tmp/dhis2_client.log")

Example output:

{"ts":"2025-09-19T14:20:01+0000","level":"INFO","logger":"dhis2_client","message":"Request GET /api/system/info params=None"}

Paging

  • Default pageSize=50.
  • get_*s() yield items across pages.
  • fetch_all() returns a list of all items.
for ou in client.get_organisation_units(level=2, fields="id,displayName"):
    ...

Collections: page-by-page iteration

All collection convenience methods (get_data_elements, get_users, get_organisation_units, get_data_sets, …) fetch results page by page from DHIS2 until all matching items are returned.

  • ✅ Safe for large DHIS2 servers (does not load everything in one huge response).
  • ✅ Transparent pass-through: you control page, pageSize, filter, fields, etc.
  • ❌ These methods do not include the pager block that DHIS2 returns. Use client.get(...) directly if you need that metadata.

Examples

Iterate over all matching data elements (fetches pages of 50 by default):

for de in client.get_data_elements(fields="id,displayName"):
    print(de["id"], de["displayName"])

Materialize in memory (not recommended for huge datasets):

des = list(client.get_data_elements(fields="id,displayName"))
print(len(des))  # total number of matching items across all pages

Get paging info (total, page count, etc.) directly from DHIS2:

raw = client.get("/api/dataElements", params={"page": 1, "pageSize": 50})
print(raw["pager"]["total"])

Sharing

This client mirrors DHIS2’s common default posture where Public access is “Can edit and view metadata” (metadata read+write). Access masks:

  • DATA_READ = rwr----- (metadata rw, data r)
  • DATA_WRITE = rwrw---- (metadata rw, data rw — typical for capture)
  • META_READ = rw------- (metadata rw, no data)
  • META_WRITE = rw------- (same string, alias for clarity)
  • NO_ACCESS = -------- (deny all)

Examples

# Grant *yourself* data write (plus metadata rw baseline) on a Program
client.grant_self_access(object_type="program", object_id="PrA123", access=DATA_WRITE)  # "rwrw----"

# Change only public access → set to metadata rw (rw-------)
client.set_public_access(object_type="dashboard", object_id="db789", public_access=META_WRITE)

# Grant user data set write access
client.set_dataset_data_write("Ds1", user_ids=["u1"], user_group_ids=["gEditors"])

# Stricter security: remove all public access
client.set_dataset_data_write("Ds2", public_access=NO_ACCESS)  # "--------"

User OrgUnit Scope

Methods are operation-specific; kwargs are just the scopes:

  • capture → /organisationUnits
  • view → /dataViewOrganisationUnits
  • tei → /teiSearchOrganisationUnits

Examples

# Current user
client.add_my_org_unit_scopes(capture=["ouCapA", "ouCapB"], view=["ouViewX"])      # append (dedupes)
client.replace_my_org_unit_scopes(tei=["ouSearch1", "ouSearch2"])                  # set exactly
client.remove_my_org_unit_scopes(capture=["ouCapB"], tei=["ouSearch2"])            # remove by ID

# Another user (UID)
uid = "u123456"
client.add_user_org_unit_scopes(uid, capture=["ouA"], view=["ouB"])
client.replace_user_org_unit_scopes(uid, capture=["ouOnly"])
client.remove_user_org_unit_scopes(uid, tei=["ouOldSearch"])

Convenience Methods

Core (raw API calls)

get(path, params=None) -> dict
post(path, json=None) -> dict
put(path, json=None) -> dict
delete(path, params=None) -> dict
list_paged(path, params=None, page_size=None, item_key=None) -> Iterable[dict]
fetch_all(path, params=None, item_key=None) -> list[dict]

System

get_system_info() -> dict

Users (read-only)

get_users(**filters) -> Iterable[dict]
get_user(uid, *, fields=None) -> dict

Organisation Units

get_organisation_units(**filters) -> Iterable[dict]
get_org_unit(uid, *, fields=None) -> dict
create_org_unit(payload) -> dict
update_org_unit(uid, payload) -> dict
delete_org_unit(uid) -> dict
get_org_unit_tree(root_uid=None, levels=None) -> dict

GeoJSON

get_organisation_units_geojson(**params) -> dict
get_org_unit_geojson(uid, **params) -> dict

Data Elements

get_data_elements(**filters) -> Iterable[dict]
get_data_element(uid, *, fields=None) -> dict
create_data_element(payload) -> dict
update_data_element(uid, payload) -> dict
delete_data_element(uid) -> dict

Data Sets

get_data_sets(**filters) -> Iterable[dict]
get_data_set(uid, *, fields=None) -> dict
create_data_set(payload) -> dict
update_data_set(uid, payload) -> dict
delete_data_set(uid) -> dict

Data Values

get_data_value(de, pe, ou, co=None, aoc=None, cc=None, cp=None) -> dict
set_data_value(de, pe, ou, value, **kwargs) -> dict
delete_data_value(de, pe, ou, **kwargs) -> dict

Data Value Sets

get_data_value_set(params: dict) -> dict
post_data_value_set(payload: dict) -> dict

Analytics

get_analytics(table: str = "analytics", **params) -> dict

Examples

Users (read-only)

# List users
for u in client.get_users(fields="id,username", order="username:asc"):
    print(u)

# Single user
user = client.get_user("u123", fields="id,username,displayName")

Organisation Units

# Iterate OU level 2
for ou in client.get_organisation_units(level=2, fields="id,displayName"):
    print(ou)

# Single OU
ou = client.get_org_unit("ou123", fields="id,displayName")

# Create/Update/Delete OU
client.create_org_unit({"name": "Clinic A", "shortName": "ClinicA", "openingDate": "2020-01-01"})
client.update_org_unit("ou123", {"name": "Clinic Alpha"})
client.delete_org_unit("ou123")

# Tree
tree = client.get_org_unit_tree(root_uid="ouROOT")

GeoJSON

# Collection as GeoJSON (unpaged FeatureCollection)
fc = client.get_organisation_units_geojson(level=2, fields="id,displayName,geometry")

# Single org unit as GeoJSON
feat = client.get_org_unit_geojson("ou123", fields="id,displayName,geometry")

Data Elements

# List
for de in client.get_data_elements(fields="id,displayName", filter=["valueType:eq:INTEGER"]):
    print(de)

# CRUD
client.create_data_element({"name": "New DE", "shortName": "NDE", "valueType": "NUMBER"})
de = client.get_data_element("de123", fields="id,displayName,valueType")
client.update_data_element("de123", {"valueType": "INTEGER"})
client.delete_data_element("de123")

Data Sets

for ds in client.get_data_sets(fields="id,displayName"):
    print(ds)

ds = client.get_data_set("ds123", fields="id,displayName")
client.create_data_set({"name": "My DS", "periodType": "Monthly"})
client.update_data_set("ds123", {"name": "My DS (Updated)"})
client.delete_data_set("ds123")

Data Values

# Single data value lifecycle
client.set_data_value(de="de1", pe="202401", ou="ou1", value="42")
val = client.get_data_value(de="de1", pe="202401", ou="ou1")
client.delete_data_value(de="de1", pe="202401", ou="ou1")

Data Value Sets

# Pull a batch
dvs = client.get_data_value_set({"dataSet": "ds1", "period": "202401", "orgUnit": "ou1"})

# Push a batch
client.post_data_value_set({
  "dataSet": "ds1",
  "orgUnit": "ou1",
  "period": "202401",
  "dataValues": [
    {"dataElement": "de1", "categoryOptionCombo": "co1", "value": "5"},
    {"dataElement": "de2", "categoryOptionCombo": "co1", "value": "9"}
  ]
})

Analytics

pivot = client.get_analytics(
  dimension=["dx:de1;de2", "pe:LAST_12_MONTHS", "ou:LEVEL-2"],
  displayProperty="NAME",
  skipMeta=True,
)

Raw API calls

Not every DHIS2 endpoint has a convenience wrapper yet. You can always use the core methods to call any path directly:

# Arbitrary GET
resp = client.get("/api/indicators", params={"fields": "id,displayName"})

# Single item
indicator = client.get("/api/indicators/abc123", params={"fields": "id,displayName"})

# Create
ou = client.post("/api/organisationUnits", json={
    "name": "Clinic A",
    "shortName": "ClinicA",
    "openingDate": "2020-01-01"
})

# Update
client.put("/api/dataElements/de123", json={"valueType": "INTEGER"})

# Delete
client.delete("/api/dataSets/ds123")

# Iterate paged collection
for de in client.list_paged(
    "/api/dataElements",
    params={"fields": "id,displayName"},
    item_key="dataElements"
):
    print(de)

Testing

  • Run unit tests (mocked; no .env needed):
uv run python -m pytest -q
  • Lint/format:
ruff check .
ruff format .

Dev Setup

pip install -r requirements-dev.txt

Includes: pytest, ruff, respx, python-dotenv.


Integration Tests

Read-only integration tests (if you have credentials):

export DHIS2_BASE_URL="http://localhost:8080"
export DHIS2_USERNAME="admin"
export DHIS2_PASSWORD="district"
uv run python -m pytest -m integration -q

Destructive tests (opt-in; be careful):

export DHIS2_ALLOW_MUTATIONS=true
pytest -m integration -q tests/integration/test_live_mutations.py

Roadmap

  • Stabilize core API and paging
  • Optional async & CLI (later)
  • More helpers (e.g., file resources, indicators)

License

BSD-3-Clause

Metadata

Release files for dhis2-client 0.3.2

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dhis2-client 0.3.2
File Size Uploaded Provenance
dhis2_client-0.3.2.tar.gz 39.0 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for dhis2-client 0.3.2
File Interpreter ABI Platform
dhis2_client-0.3.2-py3-none-any.whl Python 3 none any Details

Total release size: 70.9 kB

Release files / dhis2_client-0.3.2.tar.gz

Download URL dhis2_client-0.3.2.tar.gz
Size 39.0 kB
Tags Source
SHA-256 checksum
How to use checksums
3693b628b4d90d1cce15de132dc19a9e92654f00da135e8f23993d47c74cb07f
BLAKE2b-256 checksum
How to use checksums
5d6e987a0ba36c9985c378352593be6eee268a883e1146211a4b20d532bb4e2a
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Release files / dhis2_client-0.3.2-py3-none-any.whl

Download URL dhis2_client-0.3.2-py3-none-any.whl
Size 31.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f556f7275d86b24de2dd48502d170be008ed004bc4c1c9a80c52a34844ce50bb
BLAKE2b-256 checksum
How to use checksums
8d51ee9fedf22f5db1de4f7aa5bbc19f81c4cc227b87ffcb12b9a36b1fa9ebce
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

Security for dhis2-client 0.3.2

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for dhis2_client-0.3.2.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Oct 5, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.3.2 This release

2 release files Full provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page