Skip to content

Data sets

Two accessors on Dhis2Client cover the aggregate-capture parent and its optional section tree:

Accessor API path Purpose
client.data_sets /api/dataSets Aggregate-capture parent: period type + ordered DataSetElements + optional sections + per-OU assignment.
client.sections /api/sections Ordered grouping of DEs inside one DataSet for the data-entry app.

A DataSet is a collection of DataElements captured together for one period (monthly immunisation tally, weekly commodity stock, etc.). Sections optionally group + order the DEs inside the DataSet for rendering.

DataSetElements are a join table

Wiring a DataElement into a DataSet isn't a simple ref list — the join is a DataSetElement that carries an optional per-set CategoryCombo override (the common pattern where one DE is captured under different disaggregations per set). The accessor's add_element / remove_element helpers round-trip the full DataSet, mutate dataSetElements, and PUT it back so the override travels without a dedicated endpoint:

async with Dhis2Client(...) as client:
    ds = await client.data_sets.create(
        name="ANC Monthly",
        short_name="ANCm",
        period_type="Monthly",
        open_future_periods=2,
        expiry_days=10,
    )
    await client.data_sets.add_element(ds.id, "deFirstVisit")
    await client.data_sets.add_element(ds.id, "deSecondVisit", category_combo_uid="ccAgeGroup")

Sections carry an ordered DE list

A DataSet can be sectionless (flat list) or ship multiple sections. The Data Entry app renders sections in sortOrder ascending; inside each section, DEs render in the order they appear in Section.dataElements[].

add_element appends by default, position=0 inserts at the front, reorder replaces the whole list in one PUT:

section = await client.sections.create(
    name="Vaccination",
    data_set_uid=ds.id,
    sort_order=1,
    data_element_uids=["deFirstVisit", "deSecondVisit"],
)
section = await client.sections.reorder(section.id, ["deSecondVisit", "deFirstVisit"])

No *Spec builder

Same call as the authoring triples — keyword args on the accessor.

Per-OU assignment

DataSet.organisationUnits[] is not yet exposed through a dedicated helper. The add-to-ou / remove-from-ou surface is on the roadmap as the natural next DataSet PR. For now, attach OUs via client.data_sets.update(ds) after mutating the model, or via metadata import.

Default CategoryCombo

DHIS2 rejects DataSets without a categoryCombo. Omit category_combo_uid on create to fall back to the instance's default combo (client.system.default_category_combo_uid()) — the common case.

CLI

dhis2 metadata data-sets list --period-type Monthly
dhis2 metadata data-sets create \
    --name "ANC Monthly" --short-name "ANCm" --period-type Monthly \
    --open-future-periods 2 --expiry-days 10
dhis2 metadata data-sets add-element <DS_UID> <DE_UID>
dhis2 metadata data-sets add-element <DS_UID> <DE_UID> --category-combo <CC_UID>
dhis2 metadata sections create \
    --name "Vaccination" --data-set <DS_UID> --sort-order 1 \
    --data-element <DE_A> --data-element <DE_B>
dhis2 metadata sections reorder <SECTION_UID> <DE_B> <DE_A>

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

MCP

15 tools (metadata_data_set_* + metadata_section_*) mirror the CLI surface.

data_sets

DataSet authoring — Dhis2Client.data_sets.

A DHIS2 DataSet is the collection of DataElements captured together for one period (monthly immunisation tally, weekly commodity stock, etc.). Generic CRUD lives on the generated accessor (client.resources.data_sets); this module adds the authoring primitives integration + admin flows need:

  • create(...) — named kwargs covering the minimal required subset (name, short_name, period_type) plus the optional references (category_combo, code, description) and the knobs most real DataSets carry (open_future_periods, expiry_days, timely_days).
  • add_element(ds_uid, de_uid, *, category_combo_uid=None) — append a DataElement to the DataSet, with optional per-set CategoryCombo override. DataSetElements are the join table, not a simple ref list; the accessor round-trips the full DataSet, mutates dataSetElements, and PUTs it back so the categoryCombo override can be carried without a dedicated endpoint.
  • remove_element(ds_uid, de_uid) — same round-trip, filtered.
  • rename(uid, ...) — partial-update shortcut for label fields.
  • delete(uid) — DHIS2 rejects deletes on DataSets with saved values or a section tree.

No *Spec builder — continues the spec-audit data point from the organisation-unit accessors.

Classes

DataSet

Bases: BaseModel

Generated model for DHIS2 DataSet.

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

API endpoint: /api/dataSets.

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

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

    API endpoint: /api/dataSets.

    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.")
    categoryCombo: Reference | None = Field(default=None, description="Reference to CategoryCombo.")
    code: str | None = Field(default=None, description="Unique. Length/value max=50.")
    compulsoryDataElementOperands: list[Any] | None = Field(
        default=None, description="Collection of DataElementOperand."
    )
    compulsoryFieldsCompleteOnly: bool | None = None
    created: datetime | None = None
    createdBy: Reference | None = Field(default=None, description="Reference to User.")
    dataElementDecoration: bool | None = None
    dataEntryForm: Reference | None = Field(default=None, description="Reference to DataEntryForm.")
    dataInputPeriods: list[DataInputPeriod] | None = Field(default=None, description="Collection of DataInputPeriod.")
    dataSetElements: list[DataSetElement] | None = Field(default=None, description="Collection of DataSetElement.")
    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.")
    displayOptions: str | None = Field(default=None, description="Length/value max=50000.")
    displayShortName: str | None = Field(default=None, description="Read-only.")
    expiryDays: float | 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).")
    fieldCombinationRequired: bool | None = None
    formName: str | None = Field(default=None, description="Length/value max=2147483647.")
    formType: FormType | None = Field(default=None, description="Read-only.")
    href: str | None = None
    id: str | None = Field(default=None, description="Unique. Length/value min=11, max=11.")
    indicators: list[Any] | None = Field(default=None, description="Collection of Indicator.")
    interpretations: list[Any] | None = Field(
        default=None, description="Collection of Interpretation. 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).")
    legendSets: list[Any] | None = Field(default=None, description="Collection of LegendSet.")
    mobile: bool | None = None
    name: str | None = Field(default=None, description="Length/value min=1, max=230.")
    noValueRequiresComment: bool | None = None
    notificationRecipients: Reference | None = Field(default=None, description="Reference to UserGroup.")
    notifyCompletingUser: bool | None = None
    openFuturePeriods: int | None = Field(default=None, description="Length/value max=2147483647.")
    openPeriodsAfterCoEndDate: int | None = Field(default=None, description="Length/value max=2147483647.")
    organisationUnits: list[Any] | None = Field(default=None, description="Collection of OrganisationUnit.")
    periodType: PeriodType | None = Field(default=None, description="Reference to PeriodType. Length/value max=255.")
    queryMods: Any | None = Field(default=None, description="Reference to QueryModifiers. Read-only (inverse side).")
    renderAsTabs: bool | None = None
    renderHorizontally: bool | None = None
    sections: list[Any] | None = Field(default=None, description="Collection of Section. 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.")
    skipOffline: bool | None = None
    style: Any | None = Field(default=None, description="Reference to ObjectStyle. Length/value max=255.")
    timelyDays: float | None = None
    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).")
    validCompleteOnly: bool | None = None
    version: int | None = Field(default=None, description="Length/value max=2147483647.")
    workflow: Reference | None = Field(default=None, description="Reference to DataApprovalWorkflow.")

DataSetsAccessor

Dhis2Client.data_sets — CRUD + membership helpers over /api/dataSets.

Source code in packages/dhis2w-client/src/dhis2w_client/data_sets.py
class DataSetsAccessor:
    """`Dhis2Client.data_sets` — CRUD + membership helpers over `/api/dataSets`."""

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

    async def list_all(
        self,
        *,
        period_type: PeriodType | str | None = None,
        page: int = 1,
        page_size: int = 50,
    ) -> list[DataSet]:
        """Page through DataSets, optionally filtered by periodType.

        `period_type=PeriodType.MONTHLY` narrows to monthly DataSets.
        Server-side paged — loop `page` until the returned list is
        shorter than `page_size` for the full catalog.
        """
        filters: list[str] | None = None
        if period_type is not None:
            value = period_type.value if isinstance(period_type, PeriodType) else period_type
            filters = [f"periodType:eq:{value}"]
        return cast(
            list[DataSet],
            await self._client.resources.data_sets.list(
                fields=_DS_FIELDS,
                filters=filters,
                page=page,
                page_size=page_size,
            ),
        )

    async def get(self, uid: str) -> DataSet:
        """Fetch one DataSet by UID with its DSEs + sections + OUs resolved inline."""
        return await self._client.get(f"/api/dataSets/{uid}", model=DataSet, params={"fields": _DS_FIELDS})

    async def create(
        self,
        *,
        name: str,
        short_name: str,
        period_type: PeriodType | str,
        category_combo_uid: str | None = None,
        code: str | None = None,
        form_name: str | None = None,
        description: str | None = None,
        open_future_periods: int | None = None,
        expiry_days: int | None = None,
        timely_days: int | None = None,
        uid: str | None = None,
    ) -> DataSet:
        """Create a DataSet.

        DHIS2 rejects DataSets without a `categoryCombo` — omit
        `category_combo_uid` to fall back to the default combo
        (`client.system.default_category_combo_uid()`). `period_type` is
        required (`Monthly`, `Weekly`, `Daily`, `Yearly`, …). Use the
        `PeriodType` StrEnum for typed access; a plain string is
        accepted for the rare frequency not yet in the enum.
        """
        default_combo = category_combo_uid or await self._client.system.default_category_combo_uid()
        payload: dict[str, Any] = {
            "name": name,
            "shortName": short_name,
            "periodType": period_type.value if isinstance(period_type, PeriodType) else period_type,
            "categoryCombo": {"id": default_combo},
        }
        if uid:
            payload["id"] = uid
        if code:
            payload["code"] = code
        if form_name:
            payload["formName"] = form_name
        if description:
            payload["description"] = description
        if open_future_periods is not None:
            payload["openFuturePeriods"] = open_future_periods
        if expiry_days is not None:
            payload["expiryDays"] = expiry_days
        if timely_days is not None:
            payload["timelyDays"] = timely_days
        envelope = await self._client.post("/api/dataSets", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("data-set create did not return a uid")
        return await self.get(created_uid)

    async def update(self, data_set: DataSet) -> DataSet:
        """PUT an edited DataSet back. `data_set.id` must be set."""
        if not data_set.id:
            raise ValueError("update requires data_set.id to be set")
        body = data_set.model_dump(by_alias=True, exclude_none=True, mode="json")
        _strip_self_ref_from_dse(body)
        await self._client.put_raw(f"/api/dataSets/{data_set.id}", body=body)
        return await self.get(data_set.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,
    ) -> DataSet:
        """Partial-update shortcut — read, mutate 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 add_element(
        self,
        data_set_uid: str,
        data_element_uid: str,
        *,
        category_combo_uid: str | None = None,
    ) -> DataSet:
        """Append a DataElement to the DataSet.

        Pass `category_combo_uid` to override the DE's own CategoryCombo
        for this DataSet only (a common pattern when one DE is captured
        under different disaggregations per set).
        """
        current = await self.get(data_set_uid)
        raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
        _strip_self_ref_from_dse(raw)
        existing = raw.get("dataSetElements") or []
        if any((entry.get("dataElement") or {}).get("id") == data_element_uid for entry in existing):
            return current
        new_entry: dict[str, Any] = {"dataElement": {"id": data_element_uid}}
        if category_combo_uid:
            new_entry["categoryCombo"] = {"id": category_combo_uid}
        existing.append(new_entry)
        raw["dataSetElements"] = existing
        await self._client.put_raw(f"/api/dataSets/{data_set_uid}", body=raw)
        return await self.get(data_set_uid)

    async def remove_element(self, data_set_uid: str, data_element_uid: str) -> DataSet:
        """Remove a DataElement from the DataSet."""
        current = await self.get(data_set_uid)
        raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
        _strip_self_ref_from_dse(raw)
        existing = raw.get("dataSetElements") or []
        filtered = [entry for entry in existing if (entry.get("dataElement") or {}).get("id") != data_element_uid]
        if len(filtered) == len(existing):
            return current
        raw["dataSetElements"] = filtered
        await self._client.put_raw(f"/api/dataSets/{data_set_uid}", body=raw)
        return await self.get(data_set_uid)

    async def delete(self, uid: str) -> None:
        """Delete a DataSet — DHIS2 rejects deletes on DataSets with saved values."""
        if not uid:
            raise ValueError("delete requires a non-empty uid")
        await self._client.resources.data_sets.delete(uid)
Functions
__init__(client)

Bind to the sharing client.

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

Page through DataSets, optionally filtered by periodType.

period_type=PeriodType.MONTHLY narrows to monthly DataSets. 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_sets.py
async def list_all(
    self,
    *,
    period_type: PeriodType | str | None = None,
    page: int = 1,
    page_size: int = 50,
) -> list[DataSet]:
    """Page through DataSets, optionally filtered by periodType.

    `period_type=PeriodType.MONTHLY` narrows to monthly DataSets.
    Server-side paged — loop `page` until the returned list is
    shorter than `page_size` for the full catalog.
    """
    filters: list[str] | None = None
    if period_type is not None:
        value = period_type.value if isinstance(period_type, PeriodType) else period_type
        filters = [f"periodType:eq:{value}"]
    return cast(
        list[DataSet],
        await self._client.resources.data_sets.list(
            fields=_DS_FIELDS,
            filters=filters,
            page=page,
            page_size=page_size,
        ),
    )
get(uid) async

Fetch one DataSet by UID with its DSEs + sections + OUs resolved inline.

Source code in packages/dhis2w-client/src/dhis2w_client/data_sets.py
async def get(self, uid: str) -> DataSet:
    """Fetch one DataSet by UID with its DSEs + sections + OUs resolved inline."""
    return await self._client.get(f"/api/dataSets/{uid}", model=DataSet, params={"fields": _DS_FIELDS})
create(*, name, short_name, period_type, category_combo_uid=None, code=None, form_name=None, description=None, open_future_periods=None, expiry_days=None, timely_days=None, uid=None) async

Create a DataSet.

DHIS2 rejects DataSets without a categoryCombo — omit category_combo_uid to fall back to the default combo (client.system.default_category_combo_uid()). period_type is required (Monthly, Weekly, Daily, Yearly, …). Use the PeriodType StrEnum for typed access; a plain string is accepted for the rare frequency not yet in the enum.

Source code in packages/dhis2w-client/src/dhis2w_client/data_sets.py
async def create(
    self,
    *,
    name: str,
    short_name: str,
    period_type: PeriodType | str,
    category_combo_uid: str | None = None,
    code: str | None = None,
    form_name: str | None = None,
    description: str | None = None,
    open_future_periods: int | None = None,
    expiry_days: int | None = None,
    timely_days: int | None = None,
    uid: str | None = None,
) -> DataSet:
    """Create a DataSet.

    DHIS2 rejects DataSets without a `categoryCombo` — omit
    `category_combo_uid` to fall back to the default combo
    (`client.system.default_category_combo_uid()`). `period_type` is
    required (`Monthly`, `Weekly`, `Daily`, `Yearly`, …). Use the
    `PeriodType` StrEnum for typed access; a plain string is
    accepted for the rare frequency not yet in the enum.
    """
    default_combo = category_combo_uid or await self._client.system.default_category_combo_uid()
    payload: dict[str, Any] = {
        "name": name,
        "shortName": short_name,
        "periodType": period_type.value if isinstance(period_type, PeriodType) else period_type,
        "categoryCombo": {"id": default_combo},
    }
    if uid:
        payload["id"] = uid
    if code:
        payload["code"] = code
    if form_name:
        payload["formName"] = form_name
    if description:
        payload["description"] = description
    if open_future_periods is not None:
        payload["openFuturePeriods"] = open_future_periods
    if expiry_days is not None:
        payload["expiryDays"] = expiry_days
    if timely_days is not None:
        payload["timelyDays"] = timely_days
    envelope = await self._client.post("/api/dataSets", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("data-set create did not return a uid")
    return await self.get(created_uid)
update(data_set) async

PUT an edited DataSet back. data_set.id must be set.

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

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

Source code in packages/dhis2w-client/src/dhis2w_client/data_sets.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,
) -> DataSet:
    """Partial-update shortcut — read, mutate 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)
add_element(data_set_uid, data_element_uid, *, category_combo_uid=None) async

Append a DataElement to the DataSet.

Pass category_combo_uid to override the DE's own CategoryCombo for this DataSet only (a common pattern when one DE is captured under different disaggregations per set).

Source code in packages/dhis2w-client/src/dhis2w_client/data_sets.py
async def add_element(
    self,
    data_set_uid: str,
    data_element_uid: str,
    *,
    category_combo_uid: str | None = None,
) -> DataSet:
    """Append a DataElement to the DataSet.

    Pass `category_combo_uid` to override the DE's own CategoryCombo
    for this DataSet only (a common pattern when one DE is captured
    under different disaggregations per set).
    """
    current = await self.get(data_set_uid)
    raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
    _strip_self_ref_from_dse(raw)
    existing = raw.get("dataSetElements") or []
    if any((entry.get("dataElement") or {}).get("id") == data_element_uid for entry in existing):
        return current
    new_entry: dict[str, Any] = {"dataElement": {"id": data_element_uid}}
    if category_combo_uid:
        new_entry["categoryCombo"] = {"id": category_combo_uid}
    existing.append(new_entry)
    raw["dataSetElements"] = existing
    await self._client.put_raw(f"/api/dataSets/{data_set_uid}", body=raw)
    return await self.get(data_set_uid)
remove_element(data_set_uid, data_element_uid) async

Remove a DataElement from the DataSet.

Source code in packages/dhis2w-client/src/dhis2w_client/data_sets.py
async def remove_element(self, data_set_uid: str, data_element_uid: str) -> DataSet:
    """Remove a DataElement from the DataSet."""
    current = await self.get(data_set_uid)
    raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
    _strip_self_ref_from_dse(raw)
    existing = raw.get("dataSetElements") or []
    filtered = [entry for entry in existing if (entry.get("dataElement") or {}).get("id") != data_element_uid]
    if len(filtered) == len(existing):
        return current
    raw["dataSetElements"] = filtered
    await self._client.put_raw(f"/api/dataSets/{data_set_uid}", body=raw)
    return await self.get(data_set_uid)
delete(uid) async

Delete a DataSet — DHIS2 rejects deletes on DataSets with saved values.

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

sections

Section authoring — Dhis2Client.sections.

A DHIS2 Section groups DataElements inside a DataSet for display in the Data Entry app. A DataSet can be sectionless (flat list) or ship multiple sections (monthly supply vs stock-out, ANC vs delivery, …). Each Section carries an ordered dataElements[] reference list plus optional indicators[] for the side pane.

Generic CRUD lives on the generated accessor (client.resources.sections); this module adds the authoring primitives that matter for operators:

  • create(...) — named kwargs over name + parent data_set_uid + optional sort_order / description / common toggles.
  • list_for(data_set_uid) — narrow to one DataSet's sections in sort order.
  • add_element / remove_element / reorder — the DE-ordering primitives a section-aware authoring flow needs. reorder takes a full list of DE UIDs and sets dataElements to that ordering in one PUT.
  • rename(uid, ...) — partial-update shortcut for label fields.
  • delete(uid) — removes the section shell; DEs stay on the DataSet.

Classes

Section

Bases: BaseModel

Generated model for DHIS2 Section.

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

API endpoint: /api/sections.

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

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

    API endpoint: /api/sections.

    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.")
    categoryCombos: list[Any] | None = Field(
        default=None, description="Collection of CategoryCombo. Read-only (inverse side)."
    )
    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. Read-only (inverse side).")
    dataElements: list[Any] | None = Field(default=None, description="Collection of DataElement.")
    dataSet: Reference | None = Field(default=None, description="Reference to DataSet.")
    description: str | None = Field(default=None, description="Length/value min=2, max=2147483647.")
    disableDataElementAutoGroup: bool | None = None
    displayName: str | None = Field(default=None, description="Read-only.")
    displayOptions: str | None = Field(default=None, description="Length/value max=50000.")
    favorite: bool | None = Field(default=None, description="Read-only.")
    favorites: list[Any] | None = Field(default=None, description="Collection of String. Read-only (inverse side).")
    greyedFields: list[Any] | None = Field(default=None, description="Collection of DataElementOperand.")
    href: str | None = None
    id: str | None = Field(default=None, description="Unique. Length/value min=11, max=11.")
    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. Read-only (inverse side).")
    showColumnTotals: bool | None = None
    showRowTotals: bool | None = None
    sortOrder: int | None = Field(default=None, description="Length/value max=2147483647.")
    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).")

SectionsAccessor

Dhis2Client.sections — CRUD + ordering helpers over /api/sections.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
class SectionsAccessor:
    """`Dhis2Client.sections` — CRUD + ordering helpers over `/api/sections`."""

    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[Section]:
        """Page through Sections across every DataSet."""
        return cast(
            list[Section],
            await self._client.resources.sections.list(
                fields=_SECTION_FIELDS,
                order=["sortOrder:asc"],
                page=page,
                page_size=page_size,
            ),
        )

    async def list_for(self, data_set_uid: str) -> list[Section]:
        """Return the Sections belonging to one DataSet, in sort order."""
        return cast(
            list[Section],
            await self._client.resources.sections.list(
                fields=_SECTION_FIELDS,
                filters=[f"dataSet.id:eq:{data_set_uid}"],
                order=["sortOrder:asc"],
                paging=False,
            ),
        )

    async def get(self, uid: str) -> Section:
        """Fetch one Section by UID with its DE refs resolved inline."""
        return await self._client.get(f"/api/sections/{uid}", model=Section, params={"fields": _SECTION_FIELDS})

    async def create(
        self,
        *,
        name: str,
        data_set_uid: str,
        sort_order: int | None = None,
        description: str | None = None,
        code: str | None = None,
        data_element_uids: list[str] | None = None,
        indicator_uids: list[str] | None = None,
        show_column_totals: bool | None = None,
        show_row_totals: bool | None = None,
        uid: str | None = None,
    ) -> Section:
        """Create a Section attached to `data_set_uid`.

        `data_element_uids` seeds the ordered DE list. If omitted, the
        section starts empty and DEs can be added with `add_element` or
        `reorder` afterward. `sort_order` controls where the section
        renders in the DataSet; sections are ordered ascending.
        """
        payload: dict[str, Any] = {
            "name": name,
            "dataSet": {"id": data_set_uid},
        }
        if uid:
            payload["id"] = uid
        if sort_order is not None:
            payload["sortOrder"] = sort_order
        if description:
            payload["description"] = description
        if code:
            payload["code"] = code
        if data_element_uids:
            payload["dataElements"] = [{"id": de_uid} for de_uid in data_element_uids]
        if indicator_uids:
            payload["indicators"] = [{"id": ind_uid} for ind_uid in indicator_uids]
        if show_column_totals is not None:
            payload["showColumnTotals"] = show_column_totals
        if show_row_totals is not None:
            payload["showRowTotals"] = show_row_totals
        envelope = await self._client.post("/api/sections", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("section create did not return a uid")
        return await self.get(created_uid)

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

    async def rename(
        self,
        uid: str,
        *,
        name: str | None = None,
        description: str | None = None,
        sort_order: int | None = None,
    ) -> Section:
        """Partial-update shortcut — read, mutate label / order, PUT."""
        if name is None and description is None and sort_order is None:
            raise ValueError("rename requires at least one of name / description / sort_order")
        current = await self.get(uid)
        if name is not None:
            current.name = name
        if description is not None:
            current.description = description
        if sort_order is not None:
            current.sortOrder = sort_order
        return await self.update(current)

    async def add_element(
        self,
        section_uid: str,
        data_element_uid: str,
        *,
        position: int | None = None,
    ) -> Section:
        """Append (or insert at `position`) a DataElement to the Section.

        DHIS2 preserves the order of `dataElements[]` for data-entry
        rendering. `position` is 0-indexed; omit to append.
        """
        current = await self.get(section_uid)
        raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
        existing_ids = [ref.get("id") for ref in (raw.get("dataElements") or []) if isinstance(ref, dict)]
        if data_element_uid in existing_ids:
            return current
        refs = [{"id": ref_id} for ref_id in existing_ids if ref_id]
        if position is None or position >= len(refs):
            refs.append({"id": data_element_uid})
        else:
            refs.insert(max(position, 0), {"id": data_element_uid})
        raw["dataElements"] = refs
        await self._client.put_raw(f"/api/sections/{section_uid}", body=raw)
        return await self.get(section_uid)

    async def remove_element(self, section_uid: str, data_element_uid: str) -> Section:
        """Remove a DataElement from the Section without touching the DataSet."""
        current = await self.get(section_uid)
        raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
        existing_ids = [ref.get("id") for ref in (raw.get("dataElements") or []) if isinstance(ref, dict)]
        filtered = [ref_id for ref_id in existing_ids if ref_id and ref_id != data_element_uid]
        if len(filtered) == len(existing_ids):
            return current
        raw["dataElements"] = [{"id": ref_id} for ref_id in filtered]
        await self._client.put_raw(f"/api/sections/{section_uid}", body=raw)
        return await self.get(section_uid)

    async def reorder(self, section_uid: str, data_element_uids: list[str]) -> Section:
        """Replace the Section's `dataElements` with exactly `data_element_uids`, in order.

        Any DE UID not in the list is dropped from the Section. Missing
        UIDs aren't checked against the parent DataSet here — a
        subsequent render or validation run catches the inconsistency.
        """
        current = await self.get(section_uid)
        raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
        raw["dataElements"] = [{"id": ref_id} for ref_id in data_element_uids]
        await self._client.put_raw(f"/api/sections/{section_uid}", body=raw)
        return await self.get(section_uid)

    async def delete(self, uid: str) -> None:
        """Delete a Section — DEs stay on the parent DataSet."""
        if not uid:
            raise ValueError("delete requires a non-empty uid")
        await self._client.resources.sections.delete(uid)
Functions
__init__(client)

Bind to the sharing client.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.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 Sections across every DataSet.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def list_all(
    self,
    *,
    page: int = 1,
    page_size: int = 50,
) -> list[Section]:
    """Page through Sections across every DataSet."""
    return cast(
        list[Section],
        await self._client.resources.sections.list(
            fields=_SECTION_FIELDS,
            order=["sortOrder:asc"],
            page=page,
            page_size=page_size,
        ),
    )
list_for(data_set_uid) async

Return the Sections belonging to one DataSet, in sort order.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def list_for(self, data_set_uid: str) -> list[Section]:
    """Return the Sections belonging to one DataSet, in sort order."""
    return cast(
        list[Section],
        await self._client.resources.sections.list(
            fields=_SECTION_FIELDS,
            filters=[f"dataSet.id:eq:{data_set_uid}"],
            order=["sortOrder:asc"],
            paging=False,
        ),
    )
get(uid) async

Fetch one Section by UID with its DE refs resolved inline.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def get(self, uid: str) -> Section:
    """Fetch one Section by UID with its DE refs resolved inline."""
    return await self._client.get(f"/api/sections/{uid}", model=Section, params={"fields": _SECTION_FIELDS})
create(*, name, data_set_uid, sort_order=None, description=None, code=None, data_element_uids=None, indicator_uids=None, show_column_totals=None, show_row_totals=None, uid=None) async

Create a Section attached to data_set_uid.

data_element_uids seeds the ordered DE list. If omitted, the section starts empty and DEs can be added with add_element or reorder afterward. sort_order controls where the section renders in the DataSet; sections are ordered ascending.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def create(
    self,
    *,
    name: str,
    data_set_uid: str,
    sort_order: int | None = None,
    description: str | None = None,
    code: str | None = None,
    data_element_uids: list[str] | None = None,
    indicator_uids: list[str] | None = None,
    show_column_totals: bool | None = None,
    show_row_totals: bool | None = None,
    uid: str | None = None,
) -> Section:
    """Create a Section attached to `data_set_uid`.

    `data_element_uids` seeds the ordered DE list. If omitted, the
    section starts empty and DEs can be added with `add_element` or
    `reorder` afterward. `sort_order` controls where the section
    renders in the DataSet; sections are ordered ascending.
    """
    payload: dict[str, Any] = {
        "name": name,
        "dataSet": {"id": data_set_uid},
    }
    if uid:
        payload["id"] = uid
    if sort_order is not None:
        payload["sortOrder"] = sort_order
    if description:
        payload["description"] = description
    if code:
        payload["code"] = code
    if data_element_uids:
        payload["dataElements"] = [{"id": de_uid} for de_uid in data_element_uids]
    if indicator_uids:
        payload["indicators"] = [{"id": ind_uid} for ind_uid in indicator_uids]
    if show_column_totals is not None:
        payload["showColumnTotals"] = show_column_totals
    if show_row_totals is not None:
        payload["showRowTotals"] = show_row_totals
    envelope = await self._client.post("/api/sections", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("section create did not return a uid")
    return await self.get(created_uid)
update(section) async

PUT an edited Section back. section.id must be set.

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

Partial-update shortcut — read, mutate label / order, PUT.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def rename(
    self,
    uid: str,
    *,
    name: str | None = None,
    description: str | None = None,
    sort_order: int | None = None,
) -> Section:
    """Partial-update shortcut — read, mutate label / order, PUT."""
    if name is None and description is None and sort_order is None:
        raise ValueError("rename requires at least one of name / description / sort_order")
    current = await self.get(uid)
    if name is not None:
        current.name = name
    if description is not None:
        current.description = description
    if sort_order is not None:
        current.sortOrder = sort_order
    return await self.update(current)
add_element(section_uid, data_element_uid, *, position=None) async

Append (or insert at position) a DataElement to the Section.

DHIS2 preserves the order of dataElements[] for data-entry rendering. position is 0-indexed; omit to append.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def add_element(
    self,
    section_uid: str,
    data_element_uid: str,
    *,
    position: int | None = None,
) -> Section:
    """Append (or insert at `position`) a DataElement to the Section.

    DHIS2 preserves the order of `dataElements[]` for data-entry
    rendering. `position` is 0-indexed; omit to append.
    """
    current = await self.get(section_uid)
    raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
    existing_ids = [ref.get("id") for ref in (raw.get("dataElements") or []) if isinstance(ref, dict)]
    if data_element_uid in existing_ids:
        return current
    refs = [{"id": ref_id} for ref_id in existing_ids if ref_id]
    if position is None or position >= len(refs):
        refs.append({"id": data_element_uid})
    else:
        refs.insert(max(position, 0), {"id": data_element_uid})
    raw["dataElements"] = refs
    await self._client.put_raw(f"/api/sections/{section_uid}", body=raw)
    return await self.get(section_uid)
remove_element(section_uid, data_element_uid) async

Remove a DataElement from the Section without touching the DataSet.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def remove_element(self, section_uid: str, data_element_uid: str) -> Section:
    """Remove a DataElement from the Section without touching the DataSet."""
    current = await self.get(section_uid)
    raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
    existing_ids = [ref.get("id") for ref in (raw.get("dataElements") or []) if isinstance(ref, dict)]
    filtered = [ref_id for ref_id in existing_ids if ref_id and ref_id != data_element_uid]
    if len(filtered) == len(existing_ids):
        return current
    raw["dataElements"] = [{"id": ref_id} for ref_id in filtered]
    await self._client.put_raw(f"/api/sections/{section_uid}", body=raw)
    return await self.get(section_uid)
reorder(section_uid, data_element_uids) async

Replace the Section's dataElements with exactly data_element_uids, in order.

Any DE UID not in the list is dropped from the Section. Missing UIDs aren't checked against the parent DataSet here — a subsequent render or validation run catches the inconsistency.

Source code in packages/dhis2w-client/src/dhis2w_client/sections.py
async def reorder(self, section_uid: str, data_element_uids: list[str]) -> Section:
    """Replace the Section's `dataElements` with exactly `data_element_uids`, in order.

    Any DE UID not in the list is dropped from the Section. Missing
    UIDs aren't checked against the parent DataSet here — a
    subsequent render or validation run catches the inconsistency.
    """
    current = await self.get(section_uid)
    raw = current.model_dump(by_alias=True, exclude_none=True, mode="json")
    raw["dataElements"] = [{"id": ref_id} for ref_id in data_element_uids]
    await self._client.put_raw(f"/api/sections/{section_uid}", body=raw)
    return await self.get(section_uid)
delete(uid) async

Delete a Section — DEs stay on the parent DataSet.

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