Skip to content

Categories

client.categories — CRUD over /api/categories (the second tier of DHIS2's disaggregation model). A Category is a named grouping of CategoryOptions along one axis (e.g. "Sex", "Age band"). One or more Category records get combined into a CategoryCombo for the cross-product disaggregation actually attached to data elements.

async with Dhis2Client(...) as client:
    sex = await client.categories.create(
        name="Sex",
        short_name="Sex",
        data_dimension_type="DISAGGREGATION",
        category_option_uids=["male001UID0", "female01UID0"],
    )
    # `.add_option(category_uid, option_uid)` and `.remove_option(...)` are also available.

CRUD verbs mirror the standard pattern: list_all / get / create / update / rename / delete. The accessor wraps the typed Category model from dhis2w_client.generated.v42.schemas.category.

categories

Category authoring — Dhis2Client.categories.

A Category is one axis of a disaggregation (e.g. Sex, Age group, Modality). Each Category owns an ordered list of CategoryOptions (the values along that axis). DHIS2 then composes Categories into a CategoryCombo (the disaggregation grid) — and the cross-product of options across the combo's categories materialises as the CategoryOptionCombo set that data values key on.

This module covers the Category layer. The CategoryOption leaf already ships in dhis2w_client.category_options; CategoryCombo + the auto-generated CategoryOptionCombo matrix are the next layers in the strategic-option queue.

Generic CRUD stays on the generated accessor (client.resources.categories); this module adds keyword-arg creation with optional --options wiring at create time, partial rename, and per-item add_option / remove_option shortcuts that round-trip the ordered membership via add_collection_item / remove_collection_item.

Classes

Category

Bases: BaseModel

Generated model for DHIS2 Category.

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

API endpoint: /api/categories.

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

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

    API endpoint: /api/categories.

    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.")
    categoryCombos: list[Any] | None = Field(
        default=None, description="Collection of CategoryCombo. Read-only (inverse side)."
    )
    categoryOptions: list[Any] | None = Field(default=None, description="Collection of CategoryOption.")
    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.")
    dataDimension: bool | None = None
    dataDimensionType: DataDimensionType | None = None
    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.")

CategoriesAccessor

Dhis2Client.categories — CRUD + rename + per-item option membership helpers.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.py
class CategoriesAccessor:
    """`Dhis2Client.categories` — CRUD + rename + per-item option membership helpers."""

    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[Category]:
        """Page through Categories with categoryOptions resolved inline."""
        raw = await self._client.get_raw(
            "/api/categories",
            params={
                "fields": _CATEGORY_FIELDS,
                "page": str(page),
                "pageSize": str(page_size),
            },
        )
        return parse_collection(raw, "categories", Category)

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

    async def create(
        self,
        *,
        name: str,
        short_name: str,
        code: str | None = None,
        description: str | None = None,
        data_dimension_type: str = "DISAGGREGATION",
        options: list[str] | None = None,
        uid: str | None = None,
    ) -> Category:
        """Create a Category, optionally wiring CategoryOption members on create.

        `data_dimension_type` is `DISAGGREGATION` (the default — categories
        that participate in the data-value matrix) or `ATTRIBUTE` (categories
        used for attribute-option-combo metadata only). `options` is an
        ordered list of CategoryOption UIDs; DHIS2 preserves the order on
        save and uses it when materialising the CategoryOptionCombo matrix.
        """
        payload: dict[str, Any] = {
            "name": name,
            "shortName": short_name,
            "dataDimensionType": data_dimension_type,
        }
        if code:
            payload["code"] = code
        if description:
            payload["description"] = description
        if options:
            payload["categoryOptions"] = [{"id": option_uid} for option_uid in options]
        if uid:
            payload["id"] = uid
        envelope = await self._client.post("/api/categories", payload, model=WebMessageResponse)
        created_uid = envelope.created_uid or uid
        if not created_uid:
            raise RuntimeError("category create did not return a uid")
        return await self.get(created_uid)

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

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

    async def add_option(self, uid: str, option_uid: str) -> None:
        """Append a CategoryOption to this Category's ordered membership.

        DHIS2 preserves insertion order on the `categoryOptions` array —
        relevant when the parent CategoryCombo materialises its
        CategoryOptionCombo matrix. To re-order, edit the array via
        `update(category)` directly.
        """
        await self._client.resources.categories.add_collection_item(uid, "categoryOptions", option_uid)

    async def remove_option(self, uid: str, option_uid: str) -> None:
        """Remove a CategoryOption from this Category's membership."""
        await self._client.resources.categories.remove_collection_item(uid, "categoryOptions", option_uid)

    async def delete(self, uid: str) -> None:
        """Delete a Category — DHIS2 rejects deletes on categories referenced by a CategoryCombo."""
        if not uid:
            raise ValueError("delete requires a non-empty uid")
        await self._client.resources.categories.delete(uid)
Functions
__init__(client)

Bind to the sharing client.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.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 Categories with categoryOptions resolved inline.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.py
async def list_all(
    self,
    *,
    page: int = 1,
    page_size: int = 50,
) -> list[Category]:
    """Page through Categories with categoryOptions resolved inline."""
    raw = await self._client.get_raw(
        "/api/categories",
        params={
            "fields": _CATEGORY_FIELDS,
            "page": str(page),
            "pageSize": str(page_size),
        },
    )
    return parse_collection(raw, "categories", Category)
get(uid) async

Fetch one Category by UID.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.py
async def get(self, uid: str) -> Category:
    """Fetch one Category by UID."""
    return await self._client.get(f"/api/categories/{uid}", model=Category, params={"fields": _CATEGORY_FIELDS})
create(*, name, short_name, code=None, description=None, data_dimension_type='DISAGGREGATION', options=None, uid=None) async

Create a Category, optionally wiring CategoryOption members on create.

data_dimension_type is DISAGGREGATION (the default — categories that participate in the data-value matrix) or ATTRIBUTE (categories used for attribute-option-combo metadata only). options is an ordered list of CategoryOption UIDs; DHIS2 preserves the order on save and uses it when materialising the CategoryOptionCombo matrix.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.py
async def create(
    self,
    *,
    name: str,
    short_name: str,
    code: str | None = None,
    description: str | None = None,
    data_dimension_type: str = "DISAGGREGATION",
    options: list[str] | None = None,
    uid: str | None = None,
) -> Category:
    """Create a Category, optionally wiring CategoryOption members on create.

    `data_dimension_type` is `DISAGGREGATION` (the default — categories
    that participate in the data-value matrix) or `ATTRIBUTE` (categories
    used for attribute-option-combo metadata only). `options` is an
    ordered list of CategoryOption UIDs; DHIS2 preserves the order on
    save and uses it when materialising the CategoryOptionCombo matrix.
    """
    payload: dict[str, Any] = {
        "name": name,
        "shortName": short_name,
        "dataDimensionType": data_dimension_type,
    }
    if code:
        payload["code"] = code
    if description:
        payload["description"] = description
    if options:
        payload["categoryOptions"] = [{"id": option_uid} for option_uid in options]
    if uid:
        payload["id"] = uid
    envelope = await self._client.post("/api/categories", payload, model=WebMessageResponse)
    created_uid = envelope.created_uid or uid
    if not created_uid:
        raise RuntimeError("category create did not return a uid")
    return await self.get(created_uid)
update(category) async

PUT an edited Category back. category.id must be set.

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

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

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

Append a CategoryOption to this Category's ordered membership.

DHIS2 preserves insertion order on the categoryOptions array — relevant when the parent CategoryCombo materialises its CategoryOptionCombo matrix. To re-order, edit the array via update(category) directly.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.py
async def add_option(self, uid: str, option_uid: str) -> None:
    """Append a CategoryOption to this Category's ordered membership.

    DHIS2 preserves insertion order on the `categoryOptions` array —
    relevant when the parent CategoryCombo materialises its
    CategoryOptionCombo matrix. To re-order, edit the array via
    `update(category)` directly.
    """
    await self._client.resources.categories.add_collection_item(uid, "categoryOptions", option_uid)
remove_option(uid, option_uid) async

Remove a CategoryOption from this Category's membership.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.py
async def remove_option(self, uid: str, option_uid: str) -> None:
    """Remove a CategoryOption from this Category's membership."""
    await self._client.resources.categories.remove_collection_item(uid, "categoryOptions", option_uid)
delete(uid) async

Delete a Category — DHIS2 rejects deletes on categories referenced by a CategoryCombo.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/categories.py
async def delete(self, uid: str) -> None:
    """Delete a Category — DHIS2 rejects deletes on categories referenced by a CategoryCombo."""
    if not uid:
        raise ValueError("delete requires a non-empty uid")
    await self._client.resources.categories.delete(uid)

Functions