In this example workflow, we demonstrate how to use DHIS2 Climate Tools to make sure that DHIS2 is continuously updated with the latest climate data. Specifically we show step by step how to download and import the latest available daily precipitation data from the CHIRPS v3 data.
The notebook fetches and imports only data that is not yet present in DHIS2, making it safe to run on a recurring basis and ensuring that DHIS2 stays up to date with the latest available climate data.
If you’re only interested in downloading CHIRPS v3 data, see this detailed step-by-step guide.
Important: This notebook only aggregates to daily periods according the Gregorian calendar. Other calendar systems, like those used in Nepal or Ethiopia, are not yet supported.
Prerequisites¶
Before proceeding with the notebook, make sure the following are in place:
1. Required DHIS2 data element¶
Your DHIS2 instance must contain a data element that can receive the imported data.
For daily precipitation, the data element must have:
valueType = NUMBERaggregationType = SUMIt must belong to a data set with
periodType = DAILY
If this data element does not already exist, you have two options:
Create the data element manually in DHIS2.
Once the data element exists, copy its UID and set it as DHIS2_DATA_ELEMENT_ID in the Input Parameters section further down.
Library imports¶
We start by importing the necessary libraries:
from datetime import date
import json
import geopandas as gpd
import xarray as xr
from earthkit import transforms
from dhis2_client import DHIS2Client
from dhis2_client.settings import ClientSettings
from dhis2eo.data.chc import chirps3
from dhis2eo.integrations.pandas import dataframe_to_dhis2_jsonInput parameters¶
Let’s first define all the input parameters so they are clearly stated at the top of the notebook.
For this example we will connect to a public DHIS2 instance, so it’s important that you create the precipitation data element (as described previously) and update the DHIS2_DATA_ELEMENT_ID below. Note also that the public instance resets every night, so this process will have to be repeated for each new day.
Note that CHIRPS precipitation data is already provided in the units (millimeters) and temporal resolution (daily) we want to import into DHIS2. Therefore, this workflow does not include parameters for unit conversion or temporal aggregation.
# DHIS2 connection
DHIS2_BASE_URL = "https://play.im.dhis2.org/stable-2-42-3-1"
DHIS2_USERNAME = "admin"
DHIS2_PASSWORD = "district"
# DHIS2 import settings
DHIS2_DATA_ELEMENT_ID = '<INSERT-DATA-ELEMENT-ID-HERE>'
DHIS2_ORG_UNIT_LEVEL = 2
DHIS2_DRY_RUN = True # default to safe dry-run mode; set to False for actual import
# CHIRPS import configuration
IMPORT_VALUE_COL = "precip" # variable name in the downloaded xarray dataset
IMPORT_START_DATE = "2025-07-01" # how far back in time to start import
IMPORT_END_DATE = date.today().isoformat() # automatically tries to import the latest data
# Download settings
DOWNLOAD_FOLDER = "../../guides/data/local"
DOWNLOAD_PREFIX = "chirps3-daily" # prefix for caching downloads; existing files are reused
# Aggregation settings
SPATIAL_AGGREGATION = "mean"Connect to DHIS2¶
First, we connect the python-client to the DHIS2 instance we want to import into. You can point this to your own instance, but for the purposes of this example we will use one of the public access DHIS2 instances, since these are continuously reset:
# Client configuration
cfg = ClientSettings(
base_url=DHIS2_BASE_URL,
username=DHIS2_USERNAME,
password=DHIS2_PASSWORD
)
client = DHIS2Client(settings=cfg)
info = client.get_system_info()
# Check if everything is working.
# You should see your current DHIS2 version info.
print("Current DHIS2 version:", info["version"])Current DHIS2 version: 2.42.3.1
Get the DHIS2 organisation units¶
In order to download and aggregate the data to our DHIS2 organisation units, we also use the python-client to get the level 2 organisation units from our DHIS2 instance:
### Get org units GeoJSON from DHIS2
org_units_geojson = client.get_org_units_geojson(level=DHIS2_ORG_UNIT_LEVEL)
# Convert GeoJSON to geopandas
org_units = gpd.read_file(json.dumps(org_units_geojson))
org_unitsSkipping field groups: unsupported OGR type: 5
Check when the data was last imported¶
Since we want to run this script on a regular interval, we want to avoid importing data that has already been imported. We therefore first want to check the last date for which data was imported for the data element we want to import into. This can be done using the convenience function analytics_latest_period_for_level() provided by the python-client:
last_imported_response = client.analytics_latest_period_for_level(de_uid=DHIS2_DATA_ELEMENT_ID, level=DHIS2_ORG_UNIT_LEVEL)
last_imported_response{'meta': {'dataElement': 'UKuEMLLnoQI',
'level': 2,
'periodType': 'DAILY',
'calendar': 'iso8601',
'years_checked': 31},
'existing': None,
'next': None}Let’s extract and report the last imported month:
last_imported_period = last_imported_response["existing"]
last_imported_month_string = last_imported_period["id"][:6] if last_imported_period else None
if last_imported_month_string:
print(f"Last imported period: {last_imported_month_string}")
else:
print("No existing data found")No existing data found
We then use this information to define when we will start the data download, and ensure that we only download data after the last_imported_string:
if last_imported_month_string:
IMPORT_START_DATE_OVERRIDE = max(last_imported_month_string, IMPORT_START_DATE)
else:
IMPORT_START_DATE_OVERRIDE = IMPORT_START_DATE
print(f'Import will start at {IMPORT_START_DATE_OVERRIDE}')Import will start at 2025-07-01
Download the necessary data¶
In the next step we download all the requested data to the local file system, using convenience functionality from the dhis2eo.data.chc.chirps3 module.
Running this step may take some time depending on how many months of data are requested.
Note that after the initial data download, subsequent runs of this notebook will re-use the previously imported files to avoid repeated downloads of the same data.
For more details on this step, see our guide for Downloading CHIRPS v3 data.
print(f'Downloading data for the period: {IMPORT_START_DATE_OVERRIDE} to {IMPORT_END_DATE}...')
files = chirps3.daily.download(
start=IMPORT_START_DATE_OVERRIDE,
end=IMPORT_END_DATE,
bbox=org_units.total_bounds,
dirname=DOWNLOAD_FOLDER,
prefix=DOWNLOAD_PREFIX,
)
filesDownloading data for the period: 2025-07-01 to 2026-01-14...
INFO - 2026-01-14 10:31:01,128 - dhis2eo.data.chc.chirps3.daily - Fetching CHIRPS v3 daily from 2025-7 to 2026-1 (inclusive)
INFO - 2026-01-14 10:31:01,131 - dhis2eo.data.chc.chirps3.daily - Stage/flavor: final/rnl
INFO - 2026-01-14 10:31:01,134 - dhis2eo.data.chc.chirps3.daily - BBox: [-13.3035 6.9176 -10.2658 10.0004]
INFO - 2026-01-14 10:31:01,136 - dhis2eo.data.chc.chirps3.daily - Month 2025-7
INFO - 2026-01-14 10:31:01,141 - dhis2eo.data.chc.chirps3.daily - File already downloaded: C:\Users\karimba\Documents\Github\climate-tools\docs\guides\data\local\chirps3-daily_2025-07.nc
INFO - 2026-01-14 10:31:01,143 - dhis2eo.data.chc.chirps3.daily - Month 2025-8
INFO - 2026-01-14 10:31:01,152 - dhis2eo.data.chc.chirps3.daily - File already downloaded: C:\Users\karimba\Documents\Github\climate-tools\docs\guides\data\local\chirps3-daily_2025-08.nc
INFO - 2026-01-14 10:31:01,155 - dhis2eo.data.chc.chirps3.daily - Month 2025-9
INFO - 2026-01-14 10:31:01,159 - dhis2eo.data.chc.chirps3.daily - File already downloaded: C:\Users\karimba\Documents\Github\climate-tools\docs\guides\data\local\chirps3-daily_2025-09.nc
INFO - 2026-01-14 10:31:01,161 - dhis2eo.data.chc.chirps3.daily - Month 2025-10
INFO - 2026-01-14 10:31:01,167 - dhis2eo.data.chc.chirps3.daily - File already downloaded: C:\Users\karimba\Documents\Github\climate-tools\docs\guides\data\local\chirps3-daily_2025-10.nc
INFO - 2026-01-14 10:31:01,170 - dhis2eo.data.chc.chirps3.daily - Month 2025-11
INFO - 2026-01-14 10:31:01,172 - dhis2eo.data.chc.chirps3.daily - File already downloaded: C:\Users\karimba\Documents\Github\climate-tools\docs\guides\data\local\chirps3-daily_2025-11.nc
INFO - 2026-01-14 10:31:01,174 - dhis2eo.data.chc.chirps3.daily - Month 2025-12
WARNING - 2026-01-14 10:31:01,175 - dhis2eo.data.chc.chirps3.daily - Skipping downloads for months that have not been published yet (after 20th of the following month).
WARNING - 2026-01-14 10:31:01,176 - dhis2eo.data.chc.chirps3.daily - Last available month in CHIRPS v3: 2025-11
INFO - 2026-01-14 10:31:01,180 - dhis2eo.data.chc.chirps3.daily - Month 2026-1
WARNING - 2026-01-14 10:31:01,184 - dhis2eo.data.chc.chirps3.daily - Skipping downloads for months that have not been published yet (after 20th of the following month).
WARNING - 2026-01-14 10:31:01,187 - dhis2eo.data.chc.chirps3.daily - Last available month in CHIRPS v3: 2025-11
[WindowsPath('C:/Users/karimba/Documents/Github/climate-tools/docs/guides/data/local/chirps3-daily_2025-07.nc'),
WindowsPath('C:/Users/karimba/Documents/Github/climate-tools/docs/guides/data/local/chirps3-daily_2025-08.nc'),
WindowsPath('C:/Users/karimba/Documents/Github/climate-tools/docs/guides/data/local/chirps3-daily_2025-09.nc'),
WindowsPath('C:/Users/karimba/Documents/Github/climate-tools/docs/guides/data/local/chirps3-daily_2025-10.nc'),
WindowsPath('C:/Users/karimba/Documents/Github/climate-tools/docs/guides/data/local/chirps3-daily_2025-11.nc')]Open the downloaded data¶
Once the data has been downloaded, we can then pass the list of files to xr.open_mfdataset(). This allows us to open and work with the data as if it were a single xarray dataset:
ds_daily = xr.open_mfdataset(files)
ds_dailyAggregate to organisation units¶
Since the data is already at the correct daily level, we can proceed directly to spatial aggregation of the gridded data to our organisation units:
print("Aggregating to organisation units...")
ds_org_units = transforms.spatial.reduce(
ds_daily,
org_units,
mask_dim="id",
how=SPATIAL_AGGREGATION,
)
ds_org_unitsAggregating to organisation units...
Post-processing¶
After aggregating the precipitation data to the desired organizational units, we convert the xarray Dataset to a Pandas DataFrame. This makes it easier to inspect the data and prepare it for subsequent post-processing:
dataframe = ds_org_units.to_dataframe().reset_index()
dataframeSince CHIRPS precipitation is already recorded in the desired millimeter import units, no unit conversion or other post-processing is needed.
Create DHIS2 payload¶
At this point we have the final data that we want to import into DHIS2. In order to submit the data to DHIS2 we first have to convert the data a standardized JSON format, which can be done with the help of the dhis2eo library:
print(f"Creating payload with {len(dataframe)} values...")
payload = dataframe_to_dhis2_json(
df=dataframe,
org_unit_col="id",
period_col="time",
value_col=IMPORT_VALUE_COL,
data_element_id=DHIS2_DATA_ELEMENT_ID,
)
payload['dataValues'][:3]Creating payload with 1989 values...
[{'orgUnit': 'O6uvpzGd5pu',
'period': '20250701',
'value': '7.1407991762',
'dataElement': 'UKuEMLLnoQI'},
{'orgUnit': 'fdc6uOvgoji',
'period': '20250701',
'value': '11.684088448',
'dataElement': 'UKuEMLLnoQI'},
{'orgUnit': 'lc3eMKXaEfw',
'period': '20250701',
'value': '4.6883696756',
'dataElement': 'UKuEMLLnoQI'}]Import to DHIS2¶
print(f"Importing payload into DHIS2 (dryrun={DHIS2_DRY_RUN})...")
res = client.post("/api/dataValueSets", json=payload, params={"dryRun": str(DHIS2_DRY_RUN).lower()})
print(f'Result: {res["response"]["importCount"]}')Importing payload into DHIS2 (dryrun=True)...
Result: {'imported': 1988, 'updated': 0, 'ignored': 0, 'deleted': 0}
We have now successfully completed a full workflow for downloading, postprocessing, aggegating, and importing daily CHIRPS v3 precipitation data into DHIS2.