Skip to content

Data elements

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

Accessor API path Purpose
client.data_elements /api/dataElements Atoms of aggregate + tracker data capture. CRUD + rename + legend-set edits.
client.data_element_groups /api/dataElementGroups Thematic groupings of DEs (vaccines, ANC indicators, HIV indicators). Per-item membership add/remove.
client.data_element_group_sets /api/dataElementGroupSets Analytics dimensions collecting groups ("Vaccine stock", "HIV programme axis").

Generic CRUD is still available on the generated accessors (client.resources.data_elements + friends). The hand-written accessors layer the keyword-arg creation shapes, partial-update renames, and per-item membership shortcuts that production flows reach for every day.

No *Spec builder

Same design decision as the organisation-unit surface: keyword args on the accessor rather than a spec-over-model hop. client.data_elements.create(name=..., value_type=..., ...) dumps a plain dict at the HTTP boundary. The DataElement wire shape doesn't need transformation work (no chart-type-aware placement, no enum fan-out, no inline children with synthesised UIDs), so kwargs are the right call here — see the Legend sets doc for the rule on when reaching for a *Spec is the right shape.

Worked example

from dhis2w_client.generated.v42.enums import DataElementDomain, ValueType

async with Dhis2Client(...) as client:
    de = await client.data_elements.create(
        name="BCG doses given (<1y)",
        short_name="BCG <1y",
        value_type=ValueType.NUMBER,
        domain_type=DataElementDomain.AGGREGATE,
        legend_set_uids=["LsDoseBand1"],
    )

    group = await client.data_element_groups.create(
        name="Immunization indicators",
        short_name="Immun Ind",
    )
    await client.data_element_groups.add_members(group.id, data_element_uids=[de.id])

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

create defaults the categoryCombo to the instance default (client.system.default_category_combo_uid()); override via category_combo_uid= when you have a disaggregation.

CLI

dhis2 metadata data-elements list --domain-type AGGREGATE
dhis2 metadata data-elements create --name "BCG doses" --short-name "BCG" --value-type NUMBER --legend-set LsDoseBand1
dhis2 metadata data-element-groups create --name "Vaccines" --short-name "Vacc"
dhis2 metadata data-element-groups add-members <GROUP_UID> --data-element <DE_UID>
dhis2 metadata data-element-group-sets create --name "Programme area" --short-name "ProgArea"
dhis2 metadata data-element-group-sets add-groups <SET_UID> --group <GROUP_UID>

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

MCP

Eighteen tools: metadata_data_element_{list,get,create,rename,set_legend_sets,delete}, metadata_data_element_group_{list,get,members,create,add_members,remove_members,delete}, metadata_data_element_group_set_{list,get,create,add_groups,remove_groups,delete}.

data_elements

DataElement authoring — Dhis2Client.data_elements.

DHIS2 DataElements are the atoms of aggregate + tracker data capture; every cell of a DataValueSet and every eventDataValue on tracker events points at one. Generic CRUD lives on the generated accessor (client.resources.data_elements); this module adds the authoring primitives integration + admin flows need:

  • create(...) — named kwargs covering the minimal required subset (name, short_name, value_type, domain_type, aggregation_type) plus the optional references (category_combo, option_set, legend_set) most real DEs carry, so callers don't hand-craft the reference payload shape.
  • update(de) — PUT with an existing typed DataElement model.
  • rename(uid, ...) — partial-update shortcut for the common case of "fix the label / short name / description" without round-tripping every field.
  • delete(uid) — DHIS2 rejects deletes on DEs with saved values.

No *Spec builder — continues the spec-audit data point from the organisation-unit accessors. Callers hand the accessor keyword args; the accessor dumps a plain dict at the HTTP boundary.

Classes

DataElement

Bases: BaseModel

Generated model for DHIS2 DataElement.

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

API endpoint: /api/dataElements.

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/data_element.py
class DataElement(BaseModel):
    """Generated model for DHIS2 `DataElement`.

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

    API endpoint: /api/dataElements.

    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).")
    aggregationLevels: list[Any] | None = Field(default=None, description="Collection of Integer.")
    aggregationType: AggregationType | None = None
    attributeValues: Any | None = Field(default=None, description="Reference to AttributeValues. Length/value max=255.")
    categoryCombo: Reference | None = Field(default=None, description="Reference to CategoryCombo.")
    code: str | None = Field(default=None, description="Unique. Length/value max=50.")
    commentOptionSet: Reference | None = Field(default=None, description="Reference to OptionSet.")
    created: datetime | None = None
    createdBy: Reference | None = Field(default=None, description="Reference to User.")
    dataElementGroups: list[Any] | None = Field(
        default=None, description="Collection of DataElementGroup. Read-only (inverse side)."
    )
    dataSetElements: list[DataSetElement] | None = Field(
        default=None, description="Collection of DataSetElement. Read-only (inverse side)."
    )
    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
    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.")
    displayShortName: str | None = Field(default=None, description="Read-only.")
    domainType: DataElementDomain | None = None
    favorite: bool | None = Field(default=None, description="Read-only.")
    favorites: list[Any] | None = Field(default=None, description="Collection of String. Read-only (inverse side).")
    fieldMask: str | None = Field(default=None, description="Length/value max=255.")
    formName: str | None = Field(default=None, description="Length/value min=2, max=230.")
    href: str | None = None
    id: str | None = Field(default=None, description="Unique. Length/value min=11, max=11.")
    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="Unique. Length/value min=1, max=230.")
    optionSet: Reference | None = Field(default=None, description="Reference to OptionSet.")
    optionSetValue: bool | None = Field(default=None, description="Read-only.")
    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="Unique. 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).")
    valueType: ValueType | None = None
    valueTypeOptions: Any | None = Field(
        default=None, description="Reference to ValueTypeOptions. Length/value max=255."
    )
    zeroIsSignificant: bool | None = None

DataElementsAccessor

Dhis2Client.data_elements — CRUD + renaming helpers over /api/dataElements.

Source code in packages/dhis2w-client/src/dhis2w_client/data_elements.py
class DataElementsAccessor:
    """`Dhis2Client.data_elements` — CRUD + renaming helpers over `/api/dataElements`."""

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

    async def list_all(
        self,
        *,
        domain_type: DataElementDomain | str | None = None,
        page: int = 1,
        page_size: int = 50,
    ) -> list[DataElement]:
        """Page through DataElements, optionally filtered to one domain.

        `domain_type=DataElementDomain.AGGREGATE` narrows to aggregate
        DEs; `TRACKER` for tracker-only. Server-side paged — loop `page`
        until the returned list is shorter than `page_size` for the full
        catalog.
        """
        filters: list[str] | None = None
        if domain_type is not None:
            value = domain_type.value if isinstance(domain_type, DataElementDomain) else domain_type
            filters = [f"domainType:eq:{value}"]
        return cast(
            list[DataElement],
            await self._client.resources.data_elements.list(
                fields=_DE_FIELDS,
                filters=filters,
                page=page,
                page_size=page_size,
            ),
        )

    async def get(self, uid: str) -> DataElement:
        """Fetch one DataElement by UID with its references resolved inline."""
        return await self._client.get(f"/api/dataElements/{uid}", model=DataElement, params={"fields": _DE_FIELDS})

    async def create(
        self,
        *,
        name: str,
        short_name: str,
        value_type: ValueType | str,
        domain_type: DataElementDomain | str = DataElementDomain.AGGREGATE,
        aggregation_type: AggregationType | str = AggregationType.SUM,
        category_combo_uid: str | None = None,
        option_set_uid: str | None = None,
        legend_set_uids: list[str] | None = None,
        code: str | None = None,
        form_name: str | None = None,
        description: str | None = None,
        uid: str | None = None,
        zero_is_significant: bool = False,
    ) -> DataElement:
        """Create an aggregate or tracker DataElement.

        DHIS2 rejects DEs without a `categoryCombo` — omit
        `category_combo_uid` to fall back to the default combo
        (`client.system.default_category_combo_uid()`). `legend_set_uids`
        wires colour-legend sets for rendering in the Data Visualizer.
        `aggregation_type` defaults to `SUM`; pass `AVERAGE_SUM_ORG_UNIT`
        / etc. via the `AggregationType` StrEnum for other modes.
        """
        default_combo = category_combo_uid or await self._client.system.default_category_combo_uid()
        payload: dict[str, Any] = {
            "name": name,
            "shortName": short_name,
            "valueType": value_type.value if isinstance(value_type, ValueType) else value_type,
            "domainType": domain_type.value if isinstance(domain_type, DataElementDomain) else domain_type,
            "aggregationType": (
                aggregation_type.value if isinstance(aggregation_type, AggregationType) else aggregation_type
            ),
            "zeroIsSignificant": zero_is_significant,
            "categoryCombo": {"id": default_combo},
        }
        if option_set_uid:
            payload["optionSet"] = {"id": option_set_uid}
        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 form_name:
            payload["formName"] = form_name
        if description:
            payload["description"] = description
        envelope = await self._client.post("/api/dataElements", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("data-element create did not return a uid")
        return await self.get(created_uid)

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

    async def rename(
        self,
        uid: str,
        *,
        name: str | None = None,
        short_name: str | None = None,
        form_name: str | None = None,
        description: str | None = None,
    ) -> DataElement:
        """Partial-update shortcut — read, mutate the label fields, PUT."""
        if name is None and short_name is None and form_name is None and description is None:
            raise ValueError("rename requires at least one of name / short_name / form_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 form_name is not None:
            current.formName = form_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]) -> DataElement:
        """Replace the legend-set refs on a DE — used to roll out threshold colouring."""
        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 delete(self, uid: str) -> None:
        """Delete a DataElement — DHIS2 rejects deletes on DEs with saved values."""
        if not uid:
            raise ValueError("delete requires a non-empty uid")
        await self._client.resources.data_elements.delete(uid)
Functions
__init__(client)

Bind to the sharing client.

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

Page through DataElements, optionally filtered to one domain.

domain_type=DataElementDomain.AGGREGATE narrows to aggregate DEs; TRACKER for tracker-only. Server-side paged — loop page until the returned list is shorter than page_size for the full catalog.

Source code in packages/dhis2w-client/src/dhis2w_client/data_elements.py
async def list_all(
    self,
    *,
    domain_type: DataElementDomain | str | None = None,
    page: int = 1,
    page_size: int = 50,
) -> list[DataElement]:
    """Page through DataElements, optionally filtered to one domain.

    `domain_type=DataElementDomain.AGGREGATE` narrows to aggregate
    DEs; `TRACKER` for tracker-only. Server-side paged — loop `page`
    until the returned list is shorter than `page_size` for the full
    catalog.
    """
    filters: list[str] | None = None
    if domain_type is not None:
        value = domain_type.value if isinstance(domain_type, DataElementDomain) else domain_type
        filters = [f"domainType:eq:{value}"]
    return cast(
        list[DataElement],
        await self._client.resources.data_elements.list(
            fields=_DE_FIELDS,
            filters=filters,
            page=page,
            page_size=page_size,
        ),
    )
get(uid) async

Fetch one DataElement by UID with its references resolved inline.

Source code in packages/dhis2w-client/src/dhis2w_client/data_elements.py
async def get(self, uid: str) -> DataElement:
    """Fetch one DataElement by UID with its references resolved inline."""
    return await self._client.get(f"/api/dataElements/{uid}", model=DataElement, params={"fields": _DE_FIELDS})
create(*, name, short_name, value_type, domain_type=DataElementDomain.AGGREGATE, aggregation_type=AggregationType.SUM, category_combo_uid=None, option_set_uid=None, legend_set_uids=None, code=None, form_name=None, description=None, uid=None, zero_is_significant=False) async

Create an aggregate or tracker DataElement.

DHIS2 rejects DEs without a categoryCombo — omit category_combo_uid to fall back to the default combo (client.system.default_category_combo_uid()). legend_set_uids wires colour-legend sets for rendering in the Data Visualizer. aggregation_type defaults to SUM; pass AVERAGE_SUM_ORG_UNIT / etc. via the AggregationType StrEnum for other modes.

Source code in packages/dhis2w-client/src/dhis2w_client/data_elements.py
async def create(
    self,
    *,
    name: str,
    short_name: str,
    value_type: ValueType | str,
    domain_type: DataElementDomain | str = DataElementDomain.AGGREGATE,
    aggregation_type: AggregationType | str = AggregationType.SUM,
    category_combo_uid: str | None = None,
    option_set_uid: str | None = None,
    legend_set_uids: list[str] | None = None,
    code: str | None = None,
    form_name: str | None = None,
    description: str | None = None,
    uid: str | None = None,
    zero_is_significant: bool = False,
) -> DataElement:
    """Create an aggregate or tracker DataElement.

    DHIS2 rejects DEs without a `categoryCombo` — omit
    `category_combo_uid` to fall back to the default combo
    (`client.system.default_category_combo_uid()`). `legend_set_uids`
    wires colour-legend sets for rendering in the Data Visualizer.
    `aggregation_type` defaults to `SUM`; pass `AVERAGE_SUM_ORG_UNIT`
    / etc. via the `AggregationType` StrEnum for other modes.
    """
    default_combo = category_combo_uid or await self._client.system.default_category_combo_uid()
    payload: dict[str, Any] = {
        "name": name,
        "shortName": short_name,
        "valueType": value_type.value if isinstance(value_type, ValueType) else value_type,
        "domainType": domain_type.value if isinstance(domain_type, DataElementDomain) else domain_type,
        "aggregationType": (
            aggregation_type.value if isinstance(aggregation_type, AggregationType) else aggregation_type
        ),
        "zeroIsSignificant": zero_is_significant,
        "categoryCombo": {"id": default_combo},
    }
    if option_set_uid:
        payload["optionSet"] = {"id": option_set_uid}
    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 form_name:
        payload["formName"] = form_name
    if description:
        payload["description"] = description
    envelope = await self._client.post("/api/dataElements", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("data-element create did not return a uid")
    return await self.get(created_uid)
update(data_element) async

PUT an edited DataElement back. data_element.id must be set.

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

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

Source code in packages/dhis2w-client/src/dhis2w_client/data_elements.py
async def rename(
    self,
    uid: str,
    *,
    name: str | None = None,
    short_name: str | None = None,
    form_name: str | None = None,
    description: str | None = None,
) -> DataElement:
    """Partial-update shortcut — read, mutate the label fields, PUT."""
    if name is None and short_name is None and form_name is None and description is None:
        raise ValueError("rename requires at least one of name / short_name / form_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 form_name is not None:
        current.formName = form_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 a DE — used to roll out threshold colouring.

Source code in packages/dhis2w-client/src/dhis2w_client/data_elements.py
async def set_legend_sets(self, uid: str, *, legend_set_uids: list[str]) -> DataElement:
    """Replace the legend-set refs on a DE — used to roll out threshold colouring."""
    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)
delete(uid) async

Delete a DataElement — DHIS2 rejects deletes on DEs with saved values.

Source code in packages/dhis2w-client/src/dhis2w_client/data_elements.py
async def delete(self, uid: str) -> None:
    """Delete a DataElement — DHIS2 rejects deletes on DEs with saved values."""
    if not uid:
        raise ValueError("delete requires a non-empty uid")
    await self._client.resources.data_elements.delete(uid)

data_element_groups

DataElementGroup authoring — Dhis2Client.data_element_groups.

DHIS2 DataElementGroups collect data elements by thematic axis (vaccines, antenatal indicators, HIV indicators, …) so dashboards, pivot tables, and bulk metadata operations can target a coherent subset in one ref. This accessor mirrors OrganisationUnitGroupsAccessor: CRUD + per-item membership add/remove via the DHIS2 collection-item shortcut routes.

Classes

DataElementGroup

Bases: BaseModel

Generated model for DHIS2 DataElementGroup.

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

API endpoint: /api/dataElementGroups.

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/data_element_group.py
class DataElementGroup(BaseModel):
    """Generated model for DHIS2 `DataElementGroup`.

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

    API endpoint: /api/dataElementGroups.

    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).")
    aggregationType: AggregationType | 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.")
    dataElements: list[Any] | None = Field(default=None, description="Collection of DataElement.")
    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
    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.")
    displayShortName: 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).")
    formName: str | None = Field(default=None, description="Length/value max=2147483647.")
    groupSets: list[Any] | None = Field(
        default=None, description="Collection of DataElementGroupSet. Read-only (inverse side)."
    )
    href: str | None = None
    id: str | None = Field(default=None, description="Unique. Length/value min=11, max=11.")
    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. Read-only (inverse side).")
    name: str | None = Field(default=None, description="Unique. Length/value min=1, max=230.")
    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="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).")

DataElementGroupsAccessor

Dhis2Client.data_element_groups — CRUD + membership helpers.

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

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

    async def list_all(self) -> list[DataElementGroup]:
        """Return every DataElementGroup with its member refs inline."""
        return cast(
            list[DataElementGroup],
            await self._client.resources.data_element_groups.list(
                fields=_DE_GROUP_FIELDS,
                paging=False,
            ),
        )

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

    async def list_members(
        self,
        uid: str,
        *,
        page: int = 1,
        page_size: int = 50,
    ) -> list[DataElement]:
        """Page through DataElements belonging to one group."""
        return cast(
            list[DataElement],
            await self._client.resources.data_elements.list(
                fields=_MEMBER_FIELDS,
                filters=[f"dataElementGroups.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,
    ) -> DataElementGroup:
        """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/dataElementGroups", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("data-element-group create did not return a uid")
        return await self.get(created_uid)

    async def update(self, group: DataElementGroup) -> DataElementGroup:
        """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/dataElementGroups/{group.id}", body=body)
        return await self.get(group.id)

    async def add_members(self, uid: str, *, data_element_uids: list[str]) -> DataElementGroup:
        """Add DataElements to the group via the per-item POST shortcut."""
        for de_uid in data_element_uids:
            await self._client.resources.data_element_groups.add_collection_item(uid, "dataElements", de_uid)
        return await self.get(uid)

    async def remove_members(self, uid: str, *, data_element_uids: list[str]) -> DataElementGroup:
        """Drop DataElements from the group via the per-item DELETE shortcut."""
        for de_uid in data_element_uids:
            await self._client.resources.data_element_groups.remove_collection_item(uid, "dataElements", de_uid)
        return await self.get(uid)

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

Bind to the sharing client.

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

Return every DataElementGroup with its member refs inline.

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

Fetch one group by UID with dataElements + groupSets populated.

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

Page through DataElements belonging to one group.

Source code in packages/dhis2w-client/src/dhis2w_client/data_element_groups.py
async def list_members(
    self,
    uid: str,
    *,
    page: int = 1,
    page_size: int = 50,
) -> list[DataElement]:
    """Page through DataElements belonging to one group."""
    return cast(
        list[DataElement],
        await self._client.resources.data_elements.list(
            fields=_MEMBER_FIELDS,
            filters=[f"dataElementGroups.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/data_element_groups.py
async def create(
    self,
    *,
    name: str,
    short_name: str,
    uid: str | None = None,
    code: str | None = None,
    description: str | None = None,
) -> DataElementGroup:
    """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/dataElementGroups", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("data-element-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/data_element_groups.py
async def update(self, group: DataElementGroup) -> DataElementGroup:
    """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/dataElementGroups/{group.id}", body=body)
    return await self.get(group.id)
add_members(uid, *, data_element_uids) async

Add DataElements to the group via the per-item POST shortcut.

Source code in packages/dhis2w-client/src/dhis2w_client/data_element_groups.py
async def add_members(self, uid: str, *, data_element_uids: list[str]) -> DataElementGroup:
    """Add DataElements to the group via the per-item POST shortcut."""
    for de_uid in data_element_uids:
        await self._client.resources.data_element_groups.add_collection_item(uid, "dataElements", de_uid)
    return await self.get(uid)
remove_members(uid, *, data_element_uids) async

Drop DataElements from the group via the per-item DELETE shortcut.

Source code in packages/dhis2w-client/src/dhis2w_client/data_element_groups.py
async def remove_members(self, uid: str, *, data_element_uids: list[str]) -> DataElementGroup:
    """Drop DataElements from the group via the per-item DELETE shortcut."""
    for de_uid in data_element_uids:
        await self._client.resources.data_element_groups.remove_collection_item(uid, "dataElements", de_uid)
    return await self.get(uid)
delete(uid) async

Delete the grouping row — member DEs stay.

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

data_element_group_sets

DataElementGroupSet authoring — Dhis2Client.data_element_group_sets.

A DataElementGroupSet is the analytics dimension that collects DataElementGroups — e.g. "Vaccine stock" carries groups for each antigen, "HIV" carries groups for testing / treatment / care indicators. Mirrors the OU-group-set surface: CRUD + per-item add_groups / remove_groups via the DHIS2 collection-item shortcut routes.

Classes

DataElementGroupSet

Bases: BaseModel

Generated model for DHIS2 DataElementGroupSet.

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

API endpoint: /api/dataElementGroupSets.

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/data_element_group_set.py
class DataElementGroupSet(BaseModel):
    """Generated model for DHIS2 `DataElementGroupSet`.

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

    API endpoint: /api/dataElementGroupSets.

    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).")
    aggregationType: AggregationType | None = None
    allItems: 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.")
    compulsory: bool | None = None
    created: datetime | None = None
    createdBy: Reference | None = Field(default=None, description="Reference to User.")
    dataDimension: bool | None = None
    dataDimensionType: DataDimensionType | None = None
    dataElementGroups: list[Any] | None = Field(default=None, description="Collection of DataElementGroup.")
    description: str | None = Field(default=None, description="Length/value min=1, max=2147483647.")
    dimension: str | None = Field(default=None, description="Length/value max=2147483647.")
    dimensionItemKeywords: Any | None = Field(
        default=None, description="Reference to DimensionItemKeywords. Read-only (inverse side)."
    )
    dimensionType: DimensionType | None = None
    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.")
    displayShortName: 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).")
    filter: str | None = Field(default=None, description="Length/value max=2147483647.")
    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.")
    items: list[Any] | None = Field(
        default=None, description="Collection of DimensionalItemObject. Read-only (inverse side)."
    )
    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).")
    name: str | None = Field(default=None, description="Unique. Length/value min=1, max=230.")
    optionSet: Reference | None = Field(default=None, description="Reference to OptionSet. Read-only (inverse side).")
    program: Reference | None = Field(default=None, description="Reference to Program. Read-only (inverse side).")
    programStage: Reference | None = Field(
        default=None, description="Reference to ProgramStage. Read-only (inverse side)."
    )
    repetition: Any | None = Field(default=None, description="Reference to EventRepetition. 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="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).")
    valueType: ValueType | None = Field(default=None, description="Read-only.")

DataElementGroupSetsAccessor

Dhis2Client.data_element_group_sets — CRUD + group-membership helpers.

Source code in packages/dhis2w-client/src/dhis2w_client/data_element_group_sets.py
class DataElementGroupSetsAccessor:
    """`Dhis2Client.data_element_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[DataElementGroupSet]:
        """Return every DataElementGroupSet with its groups inline."""
        return cast(
            list[DataElementGroupSet],
            await self._client.resources.data_element_group_sets.list(
                fields=_DE_GROUP_SET_FIELDS,
                paging=False,
            ),
        )

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

    async def list_groups(self, uid: str) -> list[DataElementGroup]:
        """Return the groups in the set in definition order."""
        group_set = await self.get(uid)
        groups = group_set.dataElementGroups or []
        return [DataElementGroup.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,
        data_dimension: bool = True,
    ) -> DataElementGroupSet:
        """Create an empty group set; wire groups into it via `add_groups`.

        `data_dimension=True` (default) exposes the set as an analytics
        axis (pivot tables, visualisations). `compulsory=True` requires
        every member DE to land in exactly one group of the set.
        """
        payload: dict[str, Any] = {
            "name": name,
            "shortName": short_name,
            "compulsory": compulsory,
            "dataDimension": data_dimension,
        }
        if uid:
            payload["id"] = uid
        if code:
            payload["code"] = code
        if description:
            payload["description"] = description
        envelope = await self._client.post("/api/dataElementGroupSets", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("data-element-group-set create did not return a uid")
        return await self.get(created_uid)

    async def update(self, group_set: DataElementGroupSet) -> DataElementGroupSet:
        """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/dataElementGroupSets/{group_set.id}", body=body)
        return await self.get(group_set.id)

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

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

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

Bind to the sharing client.

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

Return every DataElementGroupSet with its groups inline.

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

Fetch one group set by UID with its dataElementGroups populated.

Source code in packages/dhis2w-client/src/dhis2w_client/data_element_group_sets.py
async def get(self, uid: str) -> DataElementGroupSet:
    """Fetch one group set by UID with its `dataElementGroups` populated."""
    return await self._client.get(
        f"/api/dataElementGroupSets/{uid}", model=DataElementGroupSet, params={"fields": _DE_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/data_element_group_sets.py
async def list_groups(self, uid: str) -> list[DataElementGroup]:
    """Return the groups in the set in definition order."""
    group_set = await self.get(uid)
    groups = group_set.dataElementGroups or []
    return [DataElementGroup.model_validate(g) for g in groups if isinstance(g, dict)]
create(*, name, short_name, uid=None, code=None, description=None, compulsory=False, data_dimension=True) async

Create an empty group set; wire groups into it via add_groups.

data_dimension=True (default) exposes the set as an analytics axis (pivot tables, visualisations). compulsory=True requires every member DE to land in exactly one group of the set.

Source code in packages/dhis2w-client/src/dhis2w_client/data_element_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,
    data_dimension: bool = True,
) -> DataElementGroupSet:
    """Create an empty group set; wire groups into it via `add_groups`.

    `data_dimension=True` (default) exposes the set as an analytics
    axis (pivot tables, visualisations). `compulsory=True` requires
    every member DE to land in exactly one group of the set.
    """
    payload: dict[str, Any] = {
        "name": name,
        "shortName": short_name,
        "compulsory": compulsory,
        "dataDimension": data_dimension,
    }
    if uid:
        payload["id"] = uid
    if code:
        payload["code"] = code
    if description:
        payload["description"] = description
    envelope = await self._client.post("/api/dataElementGroupSets", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("data-element-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/data_element_group_sets.py
async def update(self, group_set: DataElementGroupSet) -> DataElementGroupSet:
    """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/dataElementGroupSets/{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/data_element_group_sets.py
async def add_groups(self, uid: str, *, group_uids: list[str]) -> DataElementGroupSet:
    """Add `group_uids` to the set via the per-item POST shortcut."""
    for group_uid in group_uids:
        await self._client.resources.data_element_group_sets.add_collection_item(
            uid, "dataElementGroups", 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/data_element_group_sets.py
async def remove_groups(self, uid: str, *, group_uids: list[str]) -> DataElementGroupSet:
    """Drop `group_uids` from the set via the per-item DELETE shortcut."""
    for group_uid in group_uids:
        await self._client.resources.data_element_group_sets.remove_collection_item(
            uid, "dataElementGroups", group_uid
        )
    return await self.get(uid)
delete(uid) async

Delete a group set — groups stay, only the dimension row is removed.

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