Skip to content

Aggregate data values

DataValue and DataValueSet — the typed wire shapes for DHIS2's aggregate-data path (/api/dataValueSets GET / POST and the per-value /api/dataValues endpoint). Pairs with Data values (streaming), which exposes the same shape via client.data_values.stream(...) for very large imports.

CompleteDataSetRegistration and CompleteDataSetRegistrations are the sibling completeness resource: DHIS2 records whether a data set is finished for a period separately from the values, under the same (dataSet, period, organisationUnit, attributeOptionCombo) key, at /api/completeDataSetRegistrations.

When to reach for it

  • Pushing a small or medium batch of aggregate values from a script (CSV-to-DHIS2 sync, ETL pipeline, integration test fixture).
  • Reading values back to verify a write landed or to drive an analytics-side check.
  • Building the typed payload the streaming accessor and the bulk-grouped helper both consume.
  • Marking a data set complete for a period once its values are in.

Worked example — typed write, then read-back

from dhis2w_client import DataValue, DataValueSet
from dhis2w_core.client_context import open_client
from dhis2w_core.profile import profile_from_env

async with open_client(profile_from_env()) as client:
    # Push two values. `import_grouped_by_dataset` is the cross-version
    # write path (required on v43 BUGS #35, accepted on v41 + v42). The
    # typed `DataValue`s are validated by pydantic before they hit the wire.
    values = [
        DataValue(
            dataElement="fbfJHSPpUQD",
            period="202604",
            orgUnit="ImspTQPwCqd",
            categoryOptionCombo="HllvX50cXC0",
            attributeOptionCombo="HllvX50cXC0",
            value="42",
        ),
        DataValue(
            dataElement="fbfJHSPpUQD",
            period="202605",
            orgUnit="ImspTQPwCqd",
            categoryOptionCombo="HllvX50cXC0",
            attributeOptionCombo="HllvX50cXC0",
            value="43",
        ),
    ]
    # `import_grouped_by_dataset` POSTs one envelope per DataSet group
    # (required on v43 BUGS #35). Returns a list — one WebMessageResponse
    # per POST. Aggregate the per-envelope counts to get a total.
    envelopes = await client.data_values.import_grouped_by_dataset(values)
    total_imported = sum((env.import_count().imported if env.import_count() else 0) for env in envelopes)
    print(f"posted {len(envelopes)} group(s)  total imported={total_imported}")
    for env in envelopes:
        print(f"  status={env.status}  message={env.message!r}")

    # Read back. `/api/dataValueSets` returns the typed DataValueSet shape;
    # validate the raw dict through the pydantic model.
    raw = await client.get_raw(
        "/api/dataValueSets",
        params={"dataSet": "lyLU2wR22tC", "period": "202604", "orgUnit": "ImspTQPwCqd"},
    )
    dvs = DataValueSet.model_validate(raw)
    for v in dvs.dataValues or []:
        print(f"  DE={v.dataElement}  pe={v.period}  ou={v.orgUnit}  value={v.value}")

Worked example — mark a data set complete for a period

from dhis2w_client import CompleteDataSetRegistration, CompleteDataSetRegistrations
from dhis2w_core.client_context import open_client
from dhis2w_core.profile import profile_from_env

async with open_client(profile_from_env()) as client:
    # Register completeness only once the values are in: the claim is about
    # data DHIS2 has taken, and a claim about data it refused would be false.
    registrations = CompleteDataSetRegistrations(
        completeDataSetRegistrations=[
            CompleteDataSetRegistration(
                dataSet="BfMAe6Itzgt",
                period="202604",
                organisationUnit="ImspTQPwCqd",
                # Omit attributeOptionCombo on a default-combo data set - DHIS2 fills it.
                date="2026-05-02",
                completed=True,
            )
        ]
    )
    answer = await client.post_raw(
        "/api/completeDataSetRegistrations",
        registrations.model_dump(by_alias=True, exclude_none=True, mode="json"),
    )
    print(answer["response"]["importCount"])  # {'imported': 1, ...}

    # Read the tuple back. DHIS2 answers `{}` - not an empty list - when it
    # holds no registration for it.
    stored = await client.get_raw(
        "/api/completeDataSetRegistrations",
        params={"dataSet": "BfMAe6Itzgt", "period": "202604", "orgUnit": "ImspTQPwCqd"},
    )
    print(stored.get("completeDataSetRegistrations", []))

Registering a tuple DHIS2 already holds counts updated rather than conflicting, so re-running the same registration is safe.

Worked example — read completeness back

client.complete_data_set_registrations.export is the read side of the same resource, shaped like client.data_values.export: one id or a sequence for data_set / period / org_unit, children=True for the subtree, and a start_date / end_date range in place of a period list. created and last_updated narrow to registrations filed or touched since a date.

async with open_client(profile_from_env()) as client:
    envelope = await client.complete_data_set_registrations.export(
        data_set="BfMAe6Itzgt",
        org_unit="ImspTQPwCqd",
        children=True,
        start_date="2026-01-01",
        end_date="2026-06-30",
    )
    for registration in envelope.completeDataSetRegistrations:
        print(
            f"{registration.period} {registration.organisationUnit} completed {registration.date} by {registration.storedBy}"
        )

When to use which write path

import_grouped_by_dataset(values) is the safe cross-version default. It pre-fetches each DataElement's DataSet membership and POSTs one {"dataSet": …, "dataValues": [...]} envelope per group — required on DHIS2 v43 for any DE that belongs to multiple DataSets (BUGS #35: v43 rejects mixed batches with 409 E8002). v41 + v42 accept the same envelope shape, so the call is portable.

client.data_values.stream(values, ...) is the streaming alternative for very large imports — wraps the values as an async-byte stream so httpx2 doesn't have to materialise the full payload in memory.

aggregate

Typed models for DHIS2 aggregate data values (shim over generated/v43/oas).

Covers the /api/dataValueSets GET response (a DataValueSet envelope containing a list of DataValues). The corresponding POST/import path returns a WebMessageResponse (see dhis2w_client/envelopes.py).

Covers the sibling completeness resource too: CompleteDataSetRegistration is the row /api/completeDataSetRegistrations files under the same (dataSet, period, organisationUnit, attributeOptionCombo) key the values ride, and CompleteDataSetRegistrations is the envelope a batch of them posts as.

Distinct from the generated DataElement / DataSet / CategoryOptionCombo metadata models (those come out of /api/schemas codegen) — these describe the runtime values captured against that metadata. OpenAPI ships both shapes under components/schemas/{DataValue,DataValueSet}.

Classes

DataValue

Bases: BaseModel

OpenAPI schema DataValue.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v43/oas/data_value.py
class DataValue(_BaseModel):
    """OpenAPI schema `DataValue`."""

    model_config = _ConfigDict(extra="allow", populate_by_name=True, defer_build=True)

    attributeOptionCombo: str | None = None
    categoryOptionCombo: str | None = None
    comment: str | None = None
    created: str | None = None
    dataElement: str | None = None
    deleted: bool | None = None
    followup: bool | None = None
    lastUpdated: str | None = None
    orgUnit: str | None = None
    period: str | None = None
    storedBy: str | None = None
    value: str | None = None

DataValueSet

Bases: BaseModel

OpenAPI schema DataValueSet.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v43/oas/data_value_set.py
class DataValueSet(_BaseModel):
    """OpenAPI schema `DataValueSet`."""

    model_config = _ConfigDict(extra="allow", populate_by_name=True, defer_build=True)

    attributeCategoryOptions: list[str] | None = None
    attributeOptionCombo: str | None = None
    categoryOptionComboIdScheme: str | None = None
    completeDate: str | None = None
    dataElementIdScheme: str | None = None
    dataSet: str | None = None
    dataSetIdScheme: str | None = None
    dataValues: list[DataValue] | None = None
    dryRun: bool | None = None
    idScheme: str | None = None
    orgUnit: str | None = None
    orgUnitIdScheme: str | None = None
    period: str | None = None
    strategy: str | None = None

CompleteDataSetRegistration

Bases: BaseModel

One statement that a data set is reported complete for a period, an organisation unit, and a combo.

The key is the same four facts /api/dataValueSets files values under, and completeness is a separate claim about them: the values are what was reported, and this is the reporter saying the report is finished. DHIS2 fills attributeOptionCombo with the default combo, date with today, and storedBy with the authenticated user when the write names none of them.

Hand-written rather than generated: the OpenAPI document declares /api/completeDataSetRegistrations with an untyped request body and ships no component schema for the row it carries, so there is nothing under generated/v{41,42,43}/oas to import (BUGS.md 80).

Source code in packages/dhis2w-client/src/dhis2w_client/v43/aggregate.py
class CompleteDataSetRegistration(BaseModel):
    """One statement that a data set is reported complete for a period, an organisation unit, and a combo.

    The key is the same four facts `/api/dataValueSets` files values under, and completeness is a
    separate claim about them: the values are what was reported, and this is the reporter saying the
    report is finished. DHIS2 fills `attributeOptionCombo` with the default combo, `date` with today,
    and `storedBy` with the authenticated user when the write names none of them.

    Hand-written rather than generated: the OpenAPI document declares
    `/api/completeDataSetRegistrations` with an untyped request body and ships no component schema for
    the row it carries, so there is nothing under `generated/v{41,42,43}/oas` to import (BUGS.md 80).
    """

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    dataSet: str | None = None
    period: str | None = None
    organisationUnit: str | None = None
    attributeOptionCombo: str | None = None
    date: str | None = None
    """The day the report was completed, as `YYYY-MM-DD`. DHIS2 stores today's date when none is given."""

    storedBy: str | None = None
    """Who reported it complete, stored verbatim - DHIS2 does not check it against its user table."""

    completed: bool | None = None
Attributes
date = None class-attribute instance-attribute

The day the report was completed, as YYYY-MM-DD. DHIS2 stores today's date when none is given.

storedBy = None class-attribute instance-attribute

Who reported it complete, stored verbatim - DHIS2 does not check it against its user table.

CompleteDataSetRegistrations

Bases: BaseModel

The envelope POST /api/completeDataSetRegistrations reads a batch of registrations from.

Source code in packages/dhis2w-client/src/dhis2w_client/v43/aggregate.py
class CompleteDataSetRegistrations(BaseModel):
    """The envelope `POST /api/completeDataSetRegistrations` reads a batch of registrations from."""

    model_config = ConfigDict(extra="allow", populate_by_name=True)

    completeDataSetRegistrations: list[CompleteDataSetRegistration] = Field(default_factory=list)

complete_data_set_registrations

Read access to DHIS2 dataset completeness — client.complete_data_set_registrations.

GET /api/completeDataSetRegistrations returns the completeness claims filed against the same (dataSet, period, organisationUnit, attributeOptionCombo) key /api/dataValueSets files values under. The write side posts a CompleteDataSetRegistrations envelope back; this accessor reads it and parses the same typed envelope, mirroring client.data_values.export.

Classes

CompleteDataSetRegistrationsAccessor

Dhis2Client.complete_data_set_registrations — read /api/completeDataSetRegistrations.

Source code in packages/dhis2w-client/src/dhis2w_client/v43/complete_data_set_registrations.py
class CompleteDataSetRegistrationsAccessor:
    """`Dhis2Client.complete_data_set_registrations` — read `/api/completeDataSetRegistrations`."""

    def __init__(self, client: Dhis2Client) -> None:
        """Bind to the sharing client — reuses its auth + HTTP pool for every request."""
        self._client = client

    async def export(
        self,
        *,
        data_set: str | Sequence[str] | None = None,
        period: str | Sequence[str] | None = None,
        org_unit: str | Sequence[str] | None = None,
        children: bool = False,
        start_date: str | None = None,
        end_date: str | None = None,
        created: str | None = None,
        last_updated: str | None = None,
        extra_params: Mapping[str, Any] | None = None,
    ) -> CompleteDataSetRegistrations:
        """GET `/api/completeDataSetRegistrations` and return the parsed envelope.

        The read side of the completeness resource, shaped like
        `client.data_values.export`. `data_set`, `period`, and `org_unit` each
        accept a single id or a sequence of ids, and repeat as
        `dataSet=`/`period=`/`orgUnit=` query params the way DHIS2 expects.

        - `children=True` -> `children=true`: include the org units below each
          `org_unit`.
        - `start_date` / `end_date` (`YYYY-MM-DD`): select by date range instead
          of by `period`.
        - `created` (`YYYY-MM-DD`): return only registrations filed on or after
          then.
        - `last_updated` (`YYYY-MM-DD`): return only registrations touched since
          then.
        - `extra_params` covers the rest of the surface (`idScheme`,
          `orgUnitIdScheme`, `dataSetIdScheme`, `attributeOptionComboIdScheme`,
          ...). Pass a flat mapping or a list of 2-tuples.

        Buffers the whole response into a typed `CompleteDataSetRegistrations`.

        Raises `Dhis2ApiError` on 4xx / 5xx.
        """
        params: dict[str, Any] = {}
        for key, value in (("dataSet", data_set), ("period", period), ("orgUnit", org_unit)):
            if value is None:
                continue
            params[key] = [value] if isinstance(value, str) else list(value)
        if children:
            params["children"] = "true"
        if start_date is not None:
            params["startDate"] = start_date
        if end_date is not None:
            params["endDate"] = end_date
        if created is not None:
            params["created"] = created
        if last_updated is not None:
            params["lastUpdated"] = last_updated
        if extra_params:
            params.update(extra_params)
        raw = await self._client.get_raw("/api/completeDataSetRegistrations", params=params)
        return CompleteDataSetRegistrations.model_validate(raw)
Methods:
__init__(client)

Bind to the sharing client — reuses its auth + HTTP pool for every request.

Source code in packages/dhis2w-client/src/dhis2w_client/v43/complete_data_set_registrations.py
def __init__(self, client: Dhis2Client) -> None:
    """Bind to the sharing client — reuses its auth + HTTP pool for every request."""
    self._client = client
export(*, data_set=None, period=None, org_unit=None, children=False, start_date=None, end_date=None, created=None, last_updated=None, extra_params=None) async

GET /api/completeDataSetRegistrations and return the parsed envelope.

The read side of the completeness resource, shaped like client.data_values.export. data_set, period, and org_unit each accept a single id or a sequence of ids, and repeat as dataSet=/period=/orgUnit= query params the way DHIS2 expects.

  • children=True -> children=true: include the org units below each org_unit.
  • start_date / end_date (YYYY-MM-DD): select by date range instead of by period.
  • created (YYYY-MM-DD): return only registrations filed on or after then.
  • last_updated (YYYY-MM-DD): return only registrations touched since then.
  • extra_params covers the rest of the surface (idScheme, orgUnitIdScheme, dataSetIdScheme, attributeOptionComboIdScheme, ...). Pass a flat mapping or a list of 2-tuples.

Buffers the whole response into a typed CompleteDataSetRegistrations.

Raises Dhis2ApiError on 4xx / 5xx.

Source code in packages/dhis2w-client/src/dhis2w_client/v43/complete_data_set_registrations.py
async def export(
    self,
    *,
    data_set: str | Sequence[str] | None = None,
    period: str | Sequence[str] | None = None,
    org_unit: str | Sequence[str] | None = None,
    children: bool = False,
    start_date: str | None = None,
    end_date: str | None = None,
    created: str | None = None,
    last_updated: str | None = None,
    extra_params: Mapping[str, Any] | None = None,
) -> CompleteDataSetRegistrations:
    """GET `/api/completeDataSetRegistrations` and return the parsed envelope.

    The read side of the completeness resource, shaped like
    `client.data_values.export`. `data_set`, `period`, and `org_unit` each
    accept a single id or a sequence of ids, and repeat as
    `dataSet=`/`period=`/`orgUnit=` query params the way DHIS2 expects.

    - `children=True` -> `children=true`: include the org units below each
      `org_unit`.
    - `start_date` / `end_date` (`YYYY-MM-DD`): select by date range instead
      of by `period`.
    - `created` (`YYYY-MM-DD`): return only registrations filed on or after
      then.
    - `last_updated` (`YYYY-MM-DD`): return only registrations touched since
      then.
    - `extra_params` covers the rest of the surface (`idScheme`,
      `orgUnitIdScheme`, `dataSetIdScheme`, `attributeOptionComboIdScheme`,
      ...). Pass a flat mapping or a list of 2-tuples.

    Buffers the whole response into a typed `CompleteDataSetRegistrations`.

    Raises `Dhis2ApiError` on 4xx / 5xx.
    """
    params: dict[str, Any] = {}
    for key, value in (("dataSet", data_set), ("period", period), ("orgUnit", org_unit)):
        if value is None:
            continue
        params[key] = [value] if isinstance(value, str) else list(value)
    if children:
        params["children"] = "true"
    if start_date is not None:
        params["startDate"] = start_date
    if end_date is not None:
        params["endDate"] = end_date
    if created is not None:
        params["created"] = created
    if last_updated is not None:
        params["lastUpdated"] = last_updated
    if extra_params:
        params.update(extra_params)
    raw = await self._client.get_raw("/api/completeDataSetRegistrations", params=params)
    return CompleteDataSetRegistrations.model_validate(raw)