Skip to content

Indicators

Three accessors on Dhis2Client for the Indicator triple, matching the canonical DHIS2 resource names:

Accessor API path Purpose
client.indicators /api/indicators Computed ratios / counts / percentages over DataElements. CRUD + rename + expression validation.
client.indicator_groups /api/indicatorGroups Thematic groupings (coverage, quality, mortality, …). Per-item membership.
client.indicator_group_sets /api/indicatorGroupSets Analytics dimensions collecting groups.

Generic CRUD stays on the generated accessors (client.resources.indicators, …). The hand-written accessors add keyword-arg create shapes, partial-update rename, per-item membership shortcuts, and — unique to indicators — an expression_validate(context="indicator") pre-flight that catches bad DE references before the create round-trip.

No *Spec builder

Same design call as the DataElement + organisation-unit surfaces: keyword args on the accessor rather than a spec-over-model hop. The Indicator wire shape doesn't need the kind of transformation work that motivates a spec — see the Legend sets doc for the rule on when reaching for a *Spec is the right shape.

Worked example

async with Dhis2Client(...) as client:
    # Pre-flight the expression so create doesn't fail on a typo.
    desc = await client.indicators.validate_expression("#{s46m5MS0hxu}")
    assert desc.status == "OK", desc.message

    indicator = await client.indicators.create(
        name="BCG coverage",
        short_name="BCG cov",
        indicator_type_uid="JkWynlWMjJR",  # "Number (Factor 1)"
        numerator="#{s46m5MS0hxu}",        # BCG doses given
        denominator="1",
        numerator_description="BCG doses given",
        legend_set_uids=["LsDoseBand1"],
    )

    group = await client.indicator_groups.create(
        name="Immunization coverage",
        short_name="Immun cov",
    )
    await client.indicator_groups.add_members(group.id, indicator_uids=[indicator.id])

    dimension = await client.indicator_group_sets.create(
        name="Programme area",
        short_name="ProgArea",
    )
    await client.indicator_group_sets.add_groups(dimension.id, group_uids=[group.id])

Every create defaults annualized=False; flip to True for rate-per-year indicators that should be scaled by period length on aggregation.

CLI

dhis2 metadata indicators list
dhis2 metadata indicators validate-expression "#{s46m5MS0hxu}"
dhis2 metadata indicators create \
    --name "BCG coverage" --short-name "BCG cov" \
    --indicator-type JkWynlWMjJR \
    --numerator "#{s46m5MS0hxu}" --denominator "1"
dhis2 metadata indicator-groups create --name "Immunization" --short-name "Immun"
dhis2 metadata indicator-groups add-members <GROUP_UID> --indicator <IND_UID>
dhis2 metadata indicator-group-sets create --name "ProgArea" --short-name "ProgArea"
dhis2 metadata indicator-group-sets add-groups <SET_UID> --group <GROUP_UID>

Every list has an ls alias; every destructive verb accepts --yes / -y.

MCP

Seventeen tools: metadata_indicator_{list,get,create,rename,validate_expression,set_legend_sets,delete}, metadata_indicator_group_{list,get,members,create,add_members,remove_members,delete}, metadata_indicator_group_set_{list,get,create,add_groups,remove_groups,delete}.

indicators

Indicator authoring — Dhis2Client.indicators.

DHIS2 Indicators are computed ratios / counts / percentages over DataElement values, identified by a numerator + denominator expression pair (each referencing DE UIDs via #{<uid>} + optional Category-Option-Combo refs via .{<coc_uid>}). Every indicator also carries an IndicatorType reference that pins the output scaling factor (COUNT / PERCENT / PER_100_PEOPLE / etc).

Generic CRUD lives on the generated accessor (client.resources.indicators); this module adds the authoring + validation primitives typical workflows reach for:

  • create(...) — named kwargs for the required subset (name, short_name, indicator_type_uid, numerator, denominator) plus the optional expression descriptions + legend sets.
  • update(indicator) — PUT with an existing typed model.
  • rename(uid, ...) — partial-update shortcut for label fields.
  • validate_expression(expr) — pre-flight wrapper around client.validation.describe_expression(..., context="indicator") so callers can catch bad refs before attempting a create.

Classes

Indicator

Bases: BaseModel

Generated model for DHIS2 Indicator.

DHIS2 Indicator - persisted metadata (generated from /api/schemas at DHIS2 v42).

API endpoint: /api/indicators.

Field Field(description=...) entries flag DHIS2 semantics the bare type can't capture: which side of a relationship owns the link (writable) vs the inverse side (ignored by the API), uniqueness constraints, and length bounds.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/schemas/indicator.py
class Indicator(BaseModel):
    """Generated model for DHIS2 `Indicator`.

    DHIS2 Indicator - persisted metadata (generated from /api/schemas at DHIS2 v42).

    API endpoint: /api/indicators.

    Field `Field(description=...)` entries flag DHIS2 semantics the bare
    type can't capture: which side of a relationship owns the link
    (writable) vs the inverse side (ignored by the API), uniqueness
    constraints, and length bounds.
    """

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

    access: Any | None = Field(default=None, description="Reference to Access. Read-only (inverse side).")
    aggregateExportAttributeOptionCombo: str | None = Field(default=None, description="Length/value max=255.")
    aggregateExportCategoryOptionCombo: str | None = Field(default=None, description="Length/value max=255.")
    aggregationType: AggregationType | None = None
    annualized: bool | None = None
    attributeValues: Any | None = Field(default=None, description="Reference to AttributeValues. Length/value max=255.")
    code: str | None = Field(default=None, description="Unique. Length/value max=50.")
    created: datetime | None = None
    createdBy: Reference | None = Field(default=None, description="Reference to User.")
    dataSets: list[Any] | None = Field(default=None, description="Collection of DataSet. Read-only (inverse side).")
    decimals: int | None = Field(default=None, description="Length/value max=2147483647.")
    denominator: str | None = Field(default=None, description="Length/value max=2147483647.")
    denominatorDescription: str | None = Field(default=None, description="Length/value max=2147483647.")
    description: str | None = Field(default=None, description="Length/value min=1, max=2147483647.")
    dimensionItem: str | None = Field(default=None, description="Read-only.")
    dimensionItemType: DimensionItemType | None = None
    displayDenominatorDescription: str | None = Field(default=None, description="Read-only.")
    displayDescription: str | None = Field(default=None, description="Read-only.")
    displayFormName: str | None = Field(default=None, description="Read-only.")
    displayName: str | None = Field(default=None, description="Read-only.")
    displayNumeratorDescription: str | None = Field(default=None, description="Read-only.")
    displayShortName: str | None = Field(default=None, description="Read-only.")
    explodedDenominator: str | None = Field(default=None, description="Length/value max=2147483647.")
    explodedNumerator: str | None = Field(default=None, description="Length/value max=2147483647.")
    favorite: bool | None = Field(default=None, description="Read-only.")
    favorites: list[Any] | None = Field(default=None, description="Collection of String. Read-only (inverse side).")
    formName: str | None = Field(default=None, description="Length/value max=2147483647.")
    href: str | None = None
    id: str | None = Field(default=None, description="Unique. Length/value min=11, max=11.")
    indicatorGroups: list[Any] | None = Field(
        default=None, description="Collection of IndicatorGroup. Read-only (inverse side)."
    )
    indicatorType: Reference | None = Field(default=None, description="Reference to IndicatorType.")
    lastUpdated: datetime | None = None
    lastUpdatedBy: Reference | None = Field(default=None, description="Reference to User.")
    legendSet: Reference | None = Field(default=None, description="Reference to LegendSet. Read-only (inverse side).")
    legendSets: list[Any] | None = Field(default=None, description="Collection of LegendSet.")
    name: str | None = Field(default=None, description="Length/value min=1, max=230.")
    numerator: str | None = Field(default=None, description="Length/value max=2147483647.")
    numeratorDescription: str | None = Field(default=None, description="Length/value max=2147483647.")
    queryMods: Any | None = Field(default=None, description="Reference to QueryModifiers. Read-only (inverse side).")
    sharing: Any | None = Field(default=None, description="Reference to Sharing. Length/value max=255.")
    shortName: str | None = Field(default=None, description="Length/value min=1, max=50.")
    style: Any | None = Field(default=None, description="Reference to ObjectStyle. Length/value max=255.")
    translations: list[Any] | None = Field(default=None, description="Collection of Translation. Length/value max=255.")
    url: str | None = Field(default=None, description="Length/value max=255.")
    user: Reference | None = Field(default=None, description="Reference to User. Read-only (inverse side).")

IndicatorsAccessor

Dhis2Client.indicators — CRUD + rename + expression validation.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
class IndicatorsAccessor:
    """`Dhis2Client.indicators` — CRUD + rename + expression validation."""

    def __init__(self, client: Dhis2Client) -> None:
        """Bind to the sharing client."""
        self._client = client

    async def list_all(
        self,
        *,
        page: int = 1,
        page_size: int = 50,
    ) -> list[Indicator]:
        """Page through Indicators with type + expressions resolved inline."""
        raw = await self._client.get_raw(
            "/api/indicators",
            params={
                "fields": _INDICATOR_FIELDS,
                "page": str(page),
                "pageSize": str(page_size),
            },
        )
        rows = raw.get("indicators") or []
        return [Indicator.model_validate(row) for row in rows if isinstance(row, dict)]

    async def get(self, uid: str) -> Indicator:
        """Fetch one Indicator by UID."""
        return await self._client.get(f"/api/indicators/{uid}", model=Indicator, params={"fields": _INDICATOR_FIELDS})

    async def create(
        self,
        *,
        name: str,
        short_name: str,
        indicator_type_uid: str,
        numerator: str,
        denominator: str,
        numerator_description: str | None = None,
        denominator_description: str | None = None,
        legend_set_uids: list[str] | None = None,
        annualized: bool = False,
        decimals: int | None = None,
        code: str | None = None,
        description: str | None = None,
        uid: str | None = None,
    ) -> Indicator:
        """Create an Indicator.

        `indicator_type_uid` pins the output scale (percent / count /
        etc.) via an `IndicatorType` reference. `numerator` and
        `denominator` are DHIS2 expressions — `#{<de_uid>}` for a DE,
        `#{<de_uid>.<coc_uid>}` for a DE × CategoryOptionCombo cell,
        arithmetic operators allowed. Call `validate_expression(expr)`
        first to catch typos before a failed create.
        """
        payload: dict[str, Any] = {
            "name": name,
            "shortName": short_name,
            "indicatorType": {"id": indicator_type_uid},
            "numerator": numerator,
            "denominator": denominator,
            "annualized": annualized,
        }
        if numerator_description:
            payload["numeratorDescription"] = numerator_description
        if denominator_description:
            payload["denominatorDescription"] = denominator_description
        if decimals is not None:
            payload["decimals"] = decimals
        if legend_set_uids:
            payload["legendSets"] = [{"id": uid_} for uid_ in legend_set_uids]
        if uid:
            payload["id"] = uid
        if code:
            payload["code"] = code
        if description:
            payload["description"] = description
        envelope = await self._client.post("/api/indicators", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("indicator create did not return a uid")
        return await self.get(created_uid)

    async def update(self, indicator: Indicator) -> Indicator:
        """PUT an edited Indicator back. `indicator.id` must be set."""
        if not indicator.id:
            raise ValueError("update requires indicator.id to be set")
        body = indicator.model_dump(by_alias=True, exclude_none=True, mode="json")
        await self._client.put_raw(f"/api/indicators/{indicator.id}", body=body)
        return await self.get(indicator.id)

    async def rename(
        self,
        uid: str,
        *,
        name: str | None = None,
        short_name: str | None = None,
        description: str | None = None,
    ) -> Indicator:
        """Partial-update the label fields — read, mutate, PUT."""
        if name is None and short_name is None and description is None:
            raise ValueError("rename requires at least one of name / short_name / description")
        current = await self.get(uid)
        if name is not None:
            current.name = name
        if short_name is not None:
            current.shortName = short_name
        if description is not None:
            current.description = description
        return await self.update(current)

    async def set_legend_sets(self, uid: str, *, legend_set_uids: list[str]) -> Indicator:
        """Replace the legend-set refs on one Indicator."""
        current = await self.get(uid)
        current.legendSets = [Reference(id=ref).model_dump(by_alias=True, exclude_none=True) for ref in legend_set_uids]
        return await self.update(current)

    async def validate_expression(self, expression: str) -> ExpressionDescription:
        """Parse-check a numerator / denominator expression via DHIS2's validator.

        Wraps `client.validation.describe_expression(expression,
        context="indicator")`. Returns the typed
        `ExpressionDescription` — `.status == "OK"` on success,
        `.message` names the failing reference when DHIS2 rejects.
        Cheap pre-flight for create flows that want early error
        feedback instead of a full-object POST rejection.
        """
        return await self._client.validation.describe_expression(expression, context="indicator")

    async def delete(self, uid: str) -> None:
        """Delete an Indicator — DHIS2 rejects deletes on indicators used in viz / dashboards."""
        if not uid:
            raise ValueError("delete requires a non-empty uid")
        await self._client.resources.indicators.delete(uid)
Functions
__init__(client)

Bind to the sharing client.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
def __init__(self, client: Dhis2Client) -> None:
    """Bind to the sharing client."""
    self._client = client
list_all(*, page=1, page_size=50) async

Page through Indicators with type + expressions resolved inline.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def list_all(
    self,
    *,
    page: int = 1,
    page_size: int = 50,
) -> list[Indicator]:
    """Page through Indicators with type + expressions resolved inline."""
    raw = await self._client.get_raw(
        "/api/indicators",
        params={
            "fields": _INDICATOR_FIELDS,
            "page": str(page),
            "pageSize": str(page_size),
        },
    )
    rows = raw.get("indicators") or []
    return [Indicator.model_validate(row) for row in rows if isinstance(row, dict)]
get(uid) async

Fetch one Indicator by UID.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def get(self, uid: str) -> Indicator:
    """Fetch one Indicator by UID."""
    return await self._client.get(f"/api/indicators/{uid}", model=Indicator, params={"fields": _INDICATOR_FIELDS})
create(*, name, short_name, indicator_type_uid, numerator, denominator, numerator_description=None, denominator_description=None, legend_set_uids=None, annualized=False, decimals=None, code=None, description=None, uid=None) async

Create an Indicator.

indicator_type_uid pins the output scale (percent / count / etc.) via an IndicatorType reference. numerator and denominator are DHIS2 expressions — #{<de_uid>} for a DE, #{<de_uid>.<coc_uid>} for a DE × CategoryOptionCombo cell, arithmetic operators allowed. Call validate_expression(expr) first to catch typos before a failed create.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def create(
    self,
    *,
    name: str,
    short_name: str,
    indicator_type_uid: str,
    numerator: str,
    denominator: str,
    numerator_description: str | None = None,
    denominator_description: str | None = None,
    legend_set_uids: list[str] | None = None,
    annualized: bool = False,
    decimals: int | None = None,
    code: str | None = None,
    description: str | None = None,
    uid: str | None = None,
) -> Indicator:
    """Create an Indicator.

    `indicator_type_uid` pins the output scale (percent / count /
    etc.) via an `IndicatorType` reference. `numerator` and
    `denominator` are DHIS2 expressions — `#{<de_uid>}` for a DE,
    `#{<de_uid>.<coc_uid>}` for a DE × CategoryOptionCombo cell,
    arithmetic operators allowed. Call `validate_expression(expr)`
    first to catch typos before a failed create.
    """
    payload: dict[str, Any] = {
        "name": name,
        "shortName": short_name,
        "indicatorType": {"id": indicator_type_uid},
        "numerator": numerator,
        "denominator": denominator,
        "annualized": annualized,
    }
    if numerator_description:
        payload["numeratorDescription"] = numerator_description
    if denominator_description:
        payload["denominatorDescription"] = denominator_description
    if decimals is not None:
        payload["decimals"] = decimals
    if legend_set_uids:
        payload["legendSets"] = [{"id": uid_} for uid_ in legend_set_uids]
    if uid:
        payload["id"] = uid
    if code:
        payload["code"] = code
    if description:
        payload["description"] = description
    envelope = await self._client.post("/api/indicators", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("indicator create did not return a uid")
    return await self.get(created_uid)
update(indicator) async

PUT an edited Indicator back. indicator.id must be set.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def update(self, indicator: Indicator) -> Indicator:
    """PUT an edited Indicator back. `indicator.id` must be set."""
    if not indicator.id:
        raise ValueError("update requires indicator.id to be set")
    body = indicator.model_dump(by_alias=True, exclude_none=True, mode="json")
    await self._client.put_raw(f"/api/indicators/{indicator.id}", body=body)
    return await self.get(indicator.id)
rename(uid, *, name=None, short_name=None, description=None) async

Partial-update the label fields — read, mutate, PUT.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def rename(
    self,
    uid: str,
    *,
    name: str | None = None,
    short_name: str | None = None,
    description: str | None = None,
) -> Indicator:
    """Partial-update the label fields — read, mutate, PUT."""
    if name is None and short_name is None and description is None:
        raise ValueError("rename requires at least one of name / short_name / description")
    current = await self.get(uid)
    if name is not None:
        current.name = name
    if short_name is not None:
        current.shortName = short_name
    if description is not None:
        current.description = description
    return await self.update(current)
set_legend_sets(uid, *, legend_set_uids) async

Replace the legend-set refs on one Indicator.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def set_legend_sets(self, uid: str, *, legend_set_uids: list[str]) -> Indicator:
    """Replace the legend-set refs on one Indicator."""
    current = await self.get(uid)
    current.legendSets = [Reference(id=ref).model_dump(by_alias=True, exclude_none=True) for ref in legend_set_uids]
    return await self.update(current)
validate_expression(expression) async

Parse-check a numerator / denominator expression via DHIS2's validator.

Wraps client.validation.describe_expression(expression, context="indicator"). Returns the typed ExpressionDescription.status == "OK" on success, .message names the failing reference when DHIS2 rejects. Cheap pre-flight for create flows that want early error feedback instead of a full-object POST rejection.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def validate_expression(self, expression: str) -> ExpressionDescription:
    """Parse-check a numerator / denominator expression via DHIS2's validator.

    Wraps `client.validation.describe_expression(expression,
    context="indicator")`. Returns the typed
    `ExpressionDescription` — `.status == "OK"` on success,
    `.message` names the failing reference when DHIS2 rejects.
    Cheap pre-flight for create flows that want early error
    feedback instead of a full-object POST rejection.
    """
    return await self._client.validation.describe_expression(expression, context="indicator")
delete(uid) async

Delete an Indicator — DHIS2 rejects deletes on indicators used in viz / dashboards.

Source code in packages/dhis2w-client/src/dhis2w_client/indicators.py
async def delete(self, uid: str) -> None:
    """Delete an Indicator — DHIS2 rejects deletes on indicators used in viz / dashboards."""
    if not uid:
        raise ValueError("delete requires a non-empty uid")
    await self._client.resources.indicators.delete(uid)

indicator_groups

IndicatorGroup authoring — Dhis2Client.indicator_groups.

IndicatorGroups collect indicators by thematic axis (coverage, quality, mortality, …) so dashboards and analytics can address a coherent subset in one ref. Mirrors DataElementGroupsAccessor.

Classes

IndicatorGroup

Bases: BaseModel

Generated model for DHIS2 IndicatorGroup.

DHIS2 Indicator Group - persisted metadata (generated from /api/schemas at DHIS2 v42).

API endpoint: /api/indicatorGroups.

Field Field(description=...) entries flag DHIS2 semantics the bare type can't capture: which side of a relationship owns the link (writable) vs the inverse side (ignored by the API), uniqueness constraints, and length bounds.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/schemas/indicator_group.py
class IndicatorGroup(BaseModel):
    """Generated model for DHIS2 `IndicatorGroup`.

    DHIS2 Indicator Group - persisted metadata (generated from /api/schemas at DHIS2 v42).

    API endpoint: /api/indicatorGroups.

    Field `Field(description=...)` entries flag DHIS2 semantics the bare
    type can't capture: which side of a relationship owns the link
    (writable) vs the inverse side (ignored by the API), uniqueness
    constraints, and length bounds.
    """

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

    access: Any | None = Field(default=None, description="Reference to Access. Read-only (inverse side).")
    attributeValues: Any | None = Field(default=None, description="Reference to AttributeValues. Length/value max=255.")
    code: str | None = Field(default=None, description="Unique. Length/value max=50.")
    created: datetime | None = None
    createdBy: Reference | None = Field(default=None, description="Reference to User.")
    description: str | None = Field(default=None, description="Length/value min=2, max=2147483647.")
    displayName: str | None = Field(default=None, description="Read-only.")
    favorite: bool | None = Field(default=None, description="Read-only.")
    favorites: list[Any] | None = Field(default=None, description="Collection of String. Read-only (inverse side).")
    groupSets: list[Any] | None = Field(
        default=None, description="Collection of IndicatorGroupSet. Read-only (inverse side)."
    )
    href: str | None = None
    id: str | None = Field(default=None, description="Unique. Length/value min=11, max=11.")
    indicatorGroupSet: Reference | None = Field(
        default=None, description="Reference to IndicatorGroupSet. Read-only (inverse side)."
    )
    indicators: list[Any] | None = Field(default=None, description="Collection of Indicator.")
    lastUpdated: datetime | None = None
    lastUpdatedBy: Reference | None = Field(default=None, description="Reference to User.")
    name: str | None = Field(default=None, description="Length/value min=1, max=230.")
    sharing: Any | None = Field(default=None, description="Reference to Sharing. Length/value max=255.")
    translations: list[Any] | None = Field(default=None, description="Collection of Translation. Length/value max=255.")
    user: Reference | None = Field(default=None, description="Reference to User. Read-only (inverse side).")

IndicatorGroupsAccessor

Dhis2Client.indicator_groups — CRUD + membership helpers.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
class IndicatorGroupsAccessor:
    """`Dhis2Client.indicator_groups` — CRUD + membership helpers."""

    def __init__(self, client: Dhis2Client) -> None:
        """Bind to the sharing client."""
        self._client = client

    async def list_all(self) -> list[IndicatorGroup]:
        """Return every IndicatorGroup."""
        return cast(
            list[IndicatorGroup],
            await self._client.resources.indicator_groups.list(
                fields=_INDICATOR_GROUP_FIELDS,
                paging=False,
            ),
        )

    async def get(self, uid: str) -> IndicatorGroup:
        """Fetch one group by UID with `indicators` + `groupSets` populated."""
        return await self._client.get(
            f"/api/indicatorGroups/{uid}", model=IndicatorGroup, params={"fields": _INDICATOR_GROUP_FIELDS}
        )

    async def list_members(
        self,
        uid: str,
        *,
        page: int = 1,
        page_size: int = 50,
    ) -> list[Indicator]:
        """Page through Indicators belonging to one group."""
        return cast(
            list[Indicator],
            await self._client.resources.indicators.list(
                fields=_MEMBER_FIELDS,
                filters=[f"indicatorGroups.id:eq:{uid}"],
                order=["name:asc"],
                page=page,
                page_size=page_size,
            ),
        )

    async def create(
        self,
        *,
        name: str,
        short_name: str,
        uid: str | None = None,
        code: str | None = None,
        description: str | None = None,
    ) -> IndicatorGroup:
        """Create an empty group; add members afterwards via `add_members`."""
        payload: dict[str, Any] = {"name": name, "shortName": short_name}
        if uid:
            payload["id"] = uid
        if code:
            payload["code"] = code
        if description:
            payload["description"] = description
        envelope = await self._client.post("/api/indicatorGroups", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("indicator-group create did not return a uid")
        return await self.get(created_uid)

    async def update(self, group: IndicatorGroup) -> IndicatorGroup:
        """PUT an edited group back. `group.id` must be set."""
        if not group.id:
            raise ValueError("update requires group.id to be set")
        body = group.model_dump(by_alias=True, exclude_none=True, mode="json")
        await self._client.put_raw(f"/api/indicatorGroups/{group.id}", body=body)
        return await self.get(group.id)

    async def add_members(self, uid: str, *, indicator_uids: list[str]) -> IndicatorGroup:
        """Add Indicators to the group via the generated per-item POST shortcut."""
        for ind_uid in indicator_uids:
            await self._client.resources.indicator_groups.add_collection_item(uid, "indicators", ind_uid)
        return await self.get(uid)

    async def remove_members(self, uid: str, *, indicator_uids: list[str]) -> IndicatorGroup:
        """Drop Indicators from the group via the generated per-item DELETE shortcut."""
        for ind_uid in indicator_uids:
            await self._client.resources.indicator_groups.remove_collection_item(uid, "indicators", ind_uid)
        return await self.get(uid)

    async def delete(self, uid: str) -> None:
        """Delete the grouping row — member indicators stay."""
        if not uid:
            raise ValueError("delete requires a non-empty uid")
        await self._client.resources.indicator_groups.delete(uid)
Functions
__init__(client)

Bind to the sharing client.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
def __init__(self, client: Dhis2Client) -> None:
    """Bind to the sharing client."""
    self._client = client
list_all() async

Return every IndicatorGroup.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def list_all(self) -> list[IndicatorGroup]:
    """Return every IndicatorGroup."""
    return cast(
        list[IndicatorGroup],
        await self._client.resources.indicator_groups.list(
            fields=_INDICATOR_GROUP_FIELDS,
            paging=False,
        ),
    )
get(uid) async

Fetch one group by UID with indicators + groupSets populated.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def get(self, uid: str) -> IndicatorGroup:
    """Fetch one group by UID with `indicators` + `groupSets` populated."""
    return await self._client.get(
        f"/api/indicatorGroups/{uid}", model=IndicatorGroup, params={"fields": _INDICATOR_GROUP_FIELDS}
    )
list_members(uid, *, page=1, page_size=50) async

Page through Indicators belonging to one group.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def list_members(
    self,
    uid: str,
    *,
    page: int = 1,
    page_size: int = 50,
) -> list[Indicator]:
    """Page through Indicators belonging to one group."""
    return cast(
        list[Indicator],
        await self._client.resources.indicators.list(
            fields=_MEMBER_FIELDS,
            filters=[f"indicatorGroups.id:eq:{uid}"],
            order=["name:asc"],
            page=page,
            page_size=page_size,
        ),
    )
create(*, name, short_name, uid=None, code=None, description=None) async

Create an empty group; add members afterwards via add_members.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def create(
    self,
    *,
    name: str,
    short_name: str,
    uid: str | None = None,
    code: str | None = None,
    description: str | None = None,
) -> IndicatorGroup:
    """Create an empty group; add members afterwards via `add_members`."""
    payload: dict[str, Any] = {"name": name, "shortName": short_name}
    if uid:
        payload["id"] = uid
    if code:
        payload["code"] = code
    if description:
        payload["description"] = description
    envelope = await self._client.post("/api/indicatorGroups", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("indicator-group create did not return a uid")
    return await self.get(created_uid)
update(group) async

PUT an edited group back. group.id must be set.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def update(self, group: IndicatorGroup) -> IndicatorGroup:
    """PUT an edited group back. `group.id` must be set."""
    if not group.id:
        raise ValueError("update requires group.id to be set")
    body = group.model_dump(by_alias=True, exclude_none=True, mode="json")
    await self._client.put_raw(f"/api/indicatorGroups/{group.id}", body=body)
    return await self.get(group.id)
add_members(uid, *, indicator_uids) async

Add Indicators to the group via the generated per-item POST shortcut.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def add_members(self, uid: str, *, indicator_uids: list[str]) -> IndicatorGroup:
    """Add Indicators to the group via the generated per-item POST shortcut."""
    for ind_uid in indicator_uids:
        await self._client.resources.indicator_groups.add_collection_item(uid, "indicators", ind_uid)
    return await self.get(uid)
remove_members(uid, *, indicator_uids) async

Drop Indicators from the group via the generated per-item DELETE shortcut.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def remove_members(self, uid: str, *, indicator_uids: list[str]) -> IndicatorGroup:
    """Drop Indicators from the group via the generated per-item DELETE shortcut."""
    for ind_uid in indicator_uids:
        await self._client.resources.indicator_groups.remove_collection_item(uid, "indicators", ind_uid)
    return await self.get(uid)
delete(uid) async

Delete the grouping row — member indicators stay.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_groups.py
async def delete(self, uid: str) -> None:
    """Delete the grouping row — member indicators stay."""
    if not uid:
        raise ValueError("delete requires a non-empty uid")
    await self._client.resources.indicator_groups.delete(uid)

indicator_group_sets

IndicatorGroupSet authoring — Dhis2Client.indicator_group_sets.

An IndicatorGroupSet is the analytics dimension that collects IndicatorGroups — e.g. "Programme area" carries groups for each disease programme; "Reporting quality" carries timeliness / completeness groups. Mirrors DataElementGroupSetsAccessor.

Classes

IndicatorGroupSet

Bases: BaseModel

Generated model for DHIS2 IndicatorGroupSet.

DHIS2 Indicator Group Set - persisted metadata (generated from /api/schemas at DHIS2 v42).

API endpoint: /api/indicatorGroupSets.

Field Field(description=...) entries flag DHIS2 semantics the bare type can't capture: which side of a relationship owns the link (writable) vs the inverse side (ignored by the API), uniqueness constraints, and length bounds.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/schemas/indicator_group_set.py
class IndicatorGroupSet(BaseModel):
    """Generated model for DHIS2 `IndicatorGroupSet`.

    DHIS2 Indicator Group Set - persisted metadata (generated from /api/schemas at DHIS2 v42).

    API endpoint: /api/indicatorGroupSets.

    Field `Field(description=...)` entries flag DHIS2 semantics the bare
    type can't capture: which side of a relationship owns the link
    (writable) vs the inverse side (ignored by the API), uniqueness
    constraints, and length bounds.
    """

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

    access: Any | None = Field(default=None, description="Reference to Access. Read-only (inverse side).")
    attributeValues: Any | None = Field(
        default=None, description="Reference to AttributeValues. Read-only (inverse side)."
    )
    code: str | None = Field(default=None, description="Unique. Length/value max=50.")
    compulsory: bool | None = None
    created: datetime | None = None
    createdBy: Reference | None = Field(default=None, description="Reference to User.")
    description: str | None = Field(default=None, description="Length/value min=2, max=2147483647.")
    displayName: str | None = Field(default=None, description="Read-only.")
    favorite: bool | None = Field(default=None, description="Read-only.")
    favorites: list[Any] | None = Field(default=None, description="Collection of String. Read-only (inverse side).")
    href: str | None = None
    id: str | None = Field(default=None, description="Unique. Length/value min=11, max=11.")
    indicatorGroups: list[Any] | None = Field(default=None, description="Collection of IndicatorGroup.")
    lastUpdated: datetime | None = None
    lastUpdatedBy: Reference | None = Field(default=None, description="Reference to User.")
    name: str | None = Field(default=None, description="Length/value min=1, max=230.")
    sharing: Any | None = Field(default=None, description="Reference to Sharing. Length/value max=255.")
    shortName: str | None = Field(default=None, description="Unique. Length/value min=1, max=50.")
    translations: list[Any] | None = Field(default=None, description="Collection of Translation. Length/value max=255.")
    user: Reference | None = Field(default=None, description="Reference to User. Read-only (inverse side).")

IndicatorGroupSetsAccessor

Dhis2Client.indicator_group_sets — CRUD + group-membership helpers.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
class IndicatorGroupSetsAccessor:
    """`Dhis2Client.indicator_group_sets` — CRUD + group-membership helpers."""

    def __init__(self, client: Dhis2Client) -> None:
        """Bind to the sharing client."""
        self._client = client

    async def list_all(self) -> list[IndicatorGroupSet]:
        """Return every IndicatorGroupSet with its groups inline."""
        return cast(
            list[IndicatorGroupSet],
            await self._client.resources.indicator_group_sets.list(
                fields=_INDICATOR_GROUP_SET_FIELDS,
                paging=False,
            ),
        )

    async def get(self, uid: str) -> IndicatorGroupSet:
        """Fetch one group set by UID with its `indicatorGroups` populated."""
        return await self._client.get(
            f"/api/indicatorGroupSets/{uid}", model=IndicatorGroupSet, params={"fields": _INDICATOR_GROUP_SET_FIELDS}
        )

    async def list_groups(self, uid: str) -> list[IndicatorGroup]:
        """Return the groups in the set in definition order."""
        group_set = await self.get(uid)
        groups = group_set.indicatorGroups or []
        return [IndicatorGroup.model_validate(g) for g in groups if isinstance(g, dict)]

    async def create(
        self,
        *,
        name: str,
        short_name: str,
        uid: str | None = None,
        code: str | None = None,
        description: str | None = None,
        compulsory: bool = False,
    ) -> IndicatorGroupSet:
        """Create an empty group set; wire groups in via `add_groups`."""
        payload: dict[str, Any] = {
            "name": name,
            "shortName": short_name,
            "compulsory": compulsory,
        }
        if uid:
            payload["id"] = uid
        if code:
            payload["code"] = code
        if description:
            payload["description"] = description
        envelope = await self._client.post("/api/indicatorGroupSets", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("indicator-group-set create did not return a uid")
        return await self.get(created_uid)

    async def update(self, group_set: IndicatorGroupSet) -> IndicatorGroupSet:
        """PUT an edited group set back. `group_set.id` must be set."""
        if not group_set.id:
            raise ValueError("update requires group_set.id to be set")
        body = group_set.model_dump(by_alias=True, exclude_none=True, mode="json")
        await self._client.put_raw(f"/api/indicatorGroupSets/{group_set.id}", body=body)
        return await self.get(group_set.id)

    async def add_groups(self, uid: str, *, group_uids: list[str]) -> IndicatorGroupSet:
        """Add `group_uids` to the set via the per-item POST shortcut."""
        for group_uid in group_uids:
            await self._client.resources.indicator_group_sets.add_collection_item(uid, "indicatorGroups", group_uid)
        return await self.get(uid)

    async def remove_groups(self, uid: str, *, group_uids: list[str]) -> IndicatorGroupSet:
        """Drop `group_uids` from the set via the per-item DELETE shortcut."""
        for group_uid in group_uids:
            await self._client.resources.indicator_group_sets.remove_collection_item(uid, "indicatorGroups", group_uid)
        return await self.get(uid)

    async def delete(self, uid: str) -> None:
        """Delete a group set — groups stay."""
        if not uid:
            raise ValueError("delete requires a non-empty uid")
        await self._client.resources.indicator_group_sets.delete(uid)
Functions
__init__(client)

Bind to the sharing client.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
def __init__(self, client: Dhis2Client) -> None:
    """Bind to the sharing client."""
    self._client = client
list_all() async

Return every IndicatorGroupSet with its groups inline.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def list_all(self) -> list[IndicatorGroupSet]:
    """Return every IndicatorGroupSet with its groups inline."""
    return cast(
        list[IndicatorGroupSet],
        await self._client.resources.indicator_group_sets.list(
            fields=_INDICATOR_GROUP_SET_FIELDS,
            paging=False,
        ),
    )
get(uid) async

Fetch one group set by UID with its indicatorGroups populated.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def get(self, uid: str) -> IndicatorGroupSet:
    """Fetch one group set by UID with its `indicatorGroups` populated."""
    return await self._client.get(
        f"/api/indicatorGroupSets/{uid}", model=IndicatorGroupSet, params={"fields": _INDICATOR_GROUP_SET_FIELDS}
    )
list_groups(uid) async

Return the groups in the set in definition order.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def list_groups(self, uid: str) -> list[IndicatorGroup]:
    """Return the groups in the set in definition order."""
    group_set = await self.get(uid)
    groups = group_set.indicatorGroups or []
    return [IndicatorGroup.model_validate(g) for g in groups if isinstance(g, dict)]
create(*, name, short_name, uid=None, code=None, description=None, compulsory=False) async

Create an empty group set; wire groups in via add_groups.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def create(
    self,
    *,
    name: str,
    short_name: str,
    uid: str | None = None,
    code: str | None = None,
    description: str | None = None,
    compulsory: bool = False,
) -> IndicatorGroupSet:
    """Create an empty group set; wire groups in via `add_groups`."""
    payload: dict[str, Any] = {
        "name": name,
        "shortName": short_name,
        "compulsory": compulsory,
    }
    if uid:
        payload["id"] = uid
    if code:
        payload["code"] = code
    if description:
        payload["description"] = description
    envelope = await self._client.post("/api/indicatorGroupSets", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("indicator-group-set create did not return a uid")
    return await self.get(created_uid)
update(group_set) async

PUT an edited group set back. group_set.id must be set.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def update(self, group_set: IndicatorGroupSet) -> IndicatorGroupSet:
    """PUT an edited group set back. `group_set.id` must be set."""
    if not group_set.id:
        raise ValueError("update requires group_set.id to be set")
    body = group_set.model_dump(by_alias=True, exclude_none=True, mode="json")
    await self._client.put_raw(f"/api/indicatorGroupSets/{group_set.id}", body=body)
    return await self.get(group_set.id)
add_groups(uid, *, group_uids) async

Add group_uids to the set via the per-item POST shortcut.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def add_groups(self, uid: str, *, group_uids: list[str]) -> IndicatorGroupSet:
    """Add `group_uids` to the set via the per-item POST shortcut."""
    for group_uid in group_uids:
        await self._client.resources.indicator_group_sets.add_collection_item(uid, "indicatorGroups", group_uid)
    return await self.get(uid)
remove_groups(uid, *, group_uids) async

Drop group_uids from the set via the per-item DELETE shortcut.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def remove_groups(self, uid: str, *, group_uids: list[str]) -> IndicatorGroupSet:
    """Drop `group_uids` from the set via the per-item DELETE shortcut."""
    for group_uid in group_uids:
        await self._client.resources.indicator_group_sets.remove_collection_item(uid, "indicatorGroups", group_uid)
    return await self.get(uid)
delete(uid) async

Delete a group set — groups stay.

Source code in packages/dhis2w-client/src/dhis2w_client/indicator_group_sets.py
async def delete(self, uid: str) -> None:
    """Delete a group set — groups stay."""
    if not uid:
        raise ValueError("delete requires a non-empty uid")
    await self._client.resources.indicator_group_sets.delete(uid)