Category options
Three accessors on Dhis2Client for the CategoryOption triple — the last of the four analytics-authoring triples:
| Accessor |
API path |
Purpose |
client.category_options |
/api/categoryOptions |
Disaggregation values (sex, age band, ownership, …). CRUD + rename + validity-window helper. |
client.category_option_groups |
/api/categoryOptionGroups |
Thematic groupings of options. Per-item membership add/remove. |
client.category_option_group_sets |
/api/categoryOptionGroupSets |
Analytics dimensions collecting option groups. |
Scope
This triple covers the CategoryOption layer of DHIS2's disaggregation model. The surrounding Category → CategoryCombo → CategoryOptionCombo authoring remains a strategic option on roadmap.md — those resources have tangled cross-linkage plus async regeneration of the CoC matrix on save, so they deserve their own PR rather than piggybacking on the triples pattern.
No *Spec builder
Same design call as every other triple: keyword args on the accessor.
Validity window
CategoryOption is the only one of the four triples with a startDate / endDate bound. DHIS2 rejects data-value entry against options whose window doesn't cover the period being written. The accessor exposes a dedicated helper so callers can narrow / widen the window without reaching for update(option):
async with Dhis2Client(...) as client:
co = await client.category_options.create(
name="Calendar Year 2024",
short_name="CY2024",
start_date="2024-01-01",
end_date="2024-12-31",
)
# Narrow to H1 later without reconstructing the model.
co = await client.category_options.set_validity_window(
co.id,
start_date="2024-01-01",
end_date="2024-06-30",
)
Pass None on either side of set_validity_window to clear that bound — DHIS2 treats an unset window as "always valid".
CLI
dhis2 metadata category-options list
dhis2 metadata category-options create \
--name "CY2024" --short-name "CY2024" \
--start-date 2024-01-01 --end-date 2024-12-31
dhis2 metadata category-options set-validity <CO_UID> --start-date 2024-01-01 --end-date 2024-06-30
dhis2 metadata category-option-groups create --name "Calendar years" --short-name "Years"
dhis2 metadata category-option-groups add-members <GROUP_UID> --category-option <CO_UID>
dhis2 metadata category-option-group-sets create --name "Reporting calendar" --short-name "Cal"
dhis2 metadata category-option-group-sets add-groups <SET_UID> --group <GROUP_UID>
Every list has an ls alias; every destructive verb accepts --yes / -y.
MCP
18 tools mirroring the CLI surface.
category_options
CategoryOption authoring — Dhis2Client.category_options.
CategoryOptions are the values of a Category — e.g. Male / Female
for a Sex category, <1y / 1-4y / 5-14y / 15+ for an Age group
category. DHIS2 combines categories into a CategoryCombo (the
disaggregation grid), and the cross-product of category options in a
combo becomes the CategoryOptionCombo set that data values key on.
This module covers the CategoryOption layer of that chain; the
Category / CategoryCombo / CategoryOptionCombo plumbing stays a
strategic option on the roadmap — the shallower triples in this PR
are independently useful without committing to the whole
disaggregation authoring surface.
Generic CRUD stays on the generated accessor
(client.resources.category_options); this module adds keyword-arg
creation + partial rename + a validity-window helper for
startDate / endDate (DHIS2 allows a date range that bounds when
the option is usable for data entry).
Classes
CategoryOption
Bases: BaseModel
Generated model for DHIS2 CategoryOption.
DHIS2 Category Option - persisted metadata (generated from /api/schemas at DHIS2 v42).
API endpoint: /api/categoryOptions.
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_option.py
| class CategoryOption(BaseModel):
"""Generated model for DHIS2 `CategoryOption`.
DHIS2 Category Option - persisted metadata (generated from /api/schemas at DHIS2 v42).
API endpoint: /api/categoryOptions.
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.")
categories: list[Any] | None = Field(default=None, description="Collection of Category. Read-only (inverse side).")
categoryOptionCombos: list[Any] | None = Field(
default=None, description="Collection of CategoryOptionCombo. Read-only (inverse side)."
)
categoryOptionGroups: list[Any] | None = Field(
default=None, description="Collection of CategoryOptionGroup. 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.")
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.")
endDate: datetime | 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).")
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.")
isDefault: bool | None = Field(default=None, description="Read-only.")
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.")
organisationUnits: list[Any] | None = Field(default=None, description="Collection of OrganisationUnit.")
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.")
startDate: datetime | None = None
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.")
user: Reference | None = Field(default=None, description="Reference to User. Read-only (inverse side).")
|
CategoryOptionsAccessor
Dhis2Client.category_options — CRUD + rename + validity-window helpers.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.py
| class CategoryOptionsAccessor:
"""`Dhis2Client.category_options` — CRUD + rename + validity-window 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[CategoryOption]:
"""Page through CategoryOptions with references resolved inline."""
raw = await self._client.get_raw(
"/api/categoryOptions",
params={
"fields": _CO_FIELDS,
"page": str(page),
"pageSize": str(page_size),
},
)
rows = raw.get("categoryOptions") or []
return [CategoryOption.model_validate(row) for row in rows if isinstance(row, dict)]
async def get(self, uid: str) -> CategoryOption:
"""Fetch one CategoryOption by UID."""
return await self._client.get(
f"/api/categoryOptions/{uid}", model=CategoryOption, params={"fields": _CO_FIELDS}
)
async def create(
self,
*,
name: str,
short_name: str,
code: str | None = None,
description: str | None = None,
form_name: str | None = None,
start_date: datetime | str | None = None,
end_date: datetime | str | None = None,
uid: str | None = None,
) -> CategoryOption:
"""Create a CategoryOption.
`start_date` / `end_date` bound the validity window: DHIS2 rejects
data-value entry against options whose window doesn't cover the
period. Pass ISO-8601 strings (`"2024-01-01"`) or `datetime`
instances; omit for an always-valid option.
"""
payload: dict[str, Any] = {"name": name, "shortName": short_name}
if code:
payload["code"] = code
if description:
payload["description"] = description
if form_name:
payload["formName"] = form_name
if start_date is not None:
payload["startDate"] = _serialise_date(start_date)
if end_date is not None:
payload["endDate"] = _serialise_date(end_date)
if uid:
payload["id"] = uid
envelope = await self._client.post("/api/categoryOptions", payload, model=WebMessageResponse)
created_uid = envelope.created_uid or uid
if not created_uid:
raise RuntimeError("category-option create did not return a uid")
return await self.get(created_uid)
async def update(self, option: CategoryOption) -> CategoryOption:
"""PUT an edited CategoryOption back. `option.id` must be set."""
if not option.id:
raise ValueError("update requires option.id to be set")
body = option.model_dump(by_alias=True, exclude_none=True, mode="json")
await self._client.put_raw(f"/api/categoryOptions/{option.id}", body=body)
return await self.get(option.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,
) -> CategoryOption:
"""Partial-update the label fields — read, mutate, 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_validity_window(
self,
uid: str,
*,
start_date: datetime | str | None,
end_date: datetime | str | None,
) -> CategoryOption:
"""Set the `startDate` / `endDate` validity window on a CategoryOption.
Pass `None` for either side to clear that bound. DHIS2 treats an
unset window as "always valid"; a set window rejects data-value
entry for periods outside it.
"""
current = await self.get(uid)
current.startDate = _to_datetime(start_date)
current.endDate = _to_datetime(end_date)
return await self.update(current)
async def delete(self, uid: str) -> None:
"""Delete a CategoryOption — DHIS2 rejects deletes on options in use."""
if not uid:
raise ValueError("delete requires a non-empty uid")
await self._client.resources.category_options.delete(uid)
|
Functions
__init__(client)
Bind to the sharing client.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.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 CategoryOptions with references resolved inline.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.py
| async def list_all(
self,
*,
page: int = 1,
page_size: int = 50,
) -> list[CategoryOption]:
"""Page through CategoryOptions with references resolved inline."""
raw = await self._client.get_raw(
"/api/categoryOptions",
params={
"fields": _CO_FIELDS,
"page": str(page),
"pageSize": str(page_size),
},
)
rows = raw.get("categoryOptions") or []
return [CategoryOption.model_validate(row) for row in rows if isinstance(row, dict)]
|
get(uid)
async
Fetch one CategoryOption by UID.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.py
| async def get(self, uid: str) -> CategoryOption:
"""Fetch one CategoryOption by UID."""
return await self._client.get(
f"/api/categoryOptions/{uid}", model=CategoryOption, params={"fields": _CO_FIELDS}
)
|
create(*, name, short_name, code=None, description=None, form_name=None, start_date=None, end_date=None, uid=None)
async
Create a CategoryOption.
start_date / end_date bound the validity window: DHIS2 rejects
data-value entry against options whose window doesn't cover the
period. Pass ISO-8601 strings ("2024-01-01") or datetime
instances; omit for an always-valid option.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.py
| async def create(
self,
*,
name: str,
short_name: str,
code: str | None = None,
description: str | None = None,
form_name: str | None = None,
start_date: datetime | str | None = None,
end_date: datetime | str | None = None,
uid: str | None = None,
) -> CategoryOption:
"""Create a CategoryOption.
`start_date` / `end_date` bound the validity window: DHIS2 rejects
data-value entry against options whose window doesn't cover the
period. Pass ISO-8601 strings (`"2024-01-01"`) or `datetime`
instances; omit for an always-valid option.
"""
payload: dict[str, Any] = {"name": name, "shortName": short_name}
if code:
payload["code"] = code
if description:
payload["description"] = description
if form_name:
payload["formName"] = form_name
if start_date is not None:
payload["startDate"] = _serialise_date(start_date)
if end_date is not None:
payload["endDate"] = _serialise_date(end_date)
if uid:
payload["id"] = uid
envelope = await self._client.post("/api/categoryOptions", payload, model=WebMessageResponse)
created_uid = envelope.created_uid or uid
if not created_uid:
raise RuntimeError("category-option create did not return a uid")
return await self.get(created_uid)
|
update(option)
async
PUT an edited CategoryOption back. option.id must be set.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.py
| async def update(self, option: CategoryOption) -> CategoryOption:
"""PUT an edited CategoryOption back. `option.id` must be set."""
if not option.id:
raise ValueError("update requires option.id to be set")
body = option.model_dump(by_alias=True, exclude_none=True, mode="json")
await self._client.put_raw(f"/api/categoryOptions/{option.id}", body=body)
return await self.get(option.id)
|
rename(uid, *, name=None, short_name=None, form_name=None, description=None)
async
Partial-update the label fields — read, mutate, PUT.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.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,
) -> CategoryOption:
"""Partial-update the label fields — read, mutate, 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_validity_window(uid, *, start_date, end_date)
async
Set the startDate / endDate validity window on a CategoryOption.
Pass None for either side to clear that bound. DHIS2 treats an
unset window as "always valid"; a set window rejects data-value
entry for periods outside it.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.py
| async def set_validity_window(
self,
uid: str,
*,
start_date: datetime | str | None,
end_date: datetime | str | None,
) -> CategoryOption:
"""Set the `startDate` / `endDate` validity window on a CategoryOption.
Pass `None` for either side to clear that bound. DHIS2 treats an
unset window as "always valid"; a set window rejects data-value
entry for periods outside it.
"""
current = await self.get(uid)
current.startDate = _to_datetime(start_date)
current.endDate = _to_datetime(end_date)
return await self.update(current)
|
delete(uid)
async
Delete a CategoryOption — DHIS2 rejects deletes on options in use.
Source code in packages/dhis2w-client/src/dhis2w_client/category_options.py
| async def delete(self, uid: str) -> None:
"""Delete a CategoryOption — DHIS2 rejects deletes on options in use."""
if not uid:
raise ValueError("delete requires a non-empty uid")
await self._client.resources.category_options.delete(uid)
|
category_option_groups
CategoryOptionGroup authoring — Dhis2Client.category_option_groups.
CategoryOptionGroups collect CategoryOptions thematically for
cross-disaggregation analysis (e.g. a Maternal age bands group
bundling several age CategoryOptions). Mirrors
DataElementGroupsAccessor: CRUD + per-item membership shortcuts.
Classes
CategoryOptionGroup
Bases: BaseModel
Generated model for DHIS2 CategoryOptionGroup.
DHIS2 Category Option Group - persisted metadata (generated from /api/schemas at DHIS2 v42).
API endpoint: /api/categoryOptionGroups.
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_option_group.py
| class CategoryOptionGroup(BaseModel):
"""Generated model for DHIS2 `CategoryOptionGroup`.
DHIS2 Category Option Group - persisted metadata (generated from /api/schemas at DHIS2 v42).
API endpoint: /api/categoryOptionGroups.
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.")
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.")
dataDimensionType: DataDimensionType | None = None
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 CategoryOptionGroupSet. 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).")
|
CategoryOptionGroupsAccessor
Dhis2Client.category_option_groups — CRUD + membership helpers.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| class CategoryOptionGroupsAccessor:
"""`Dhis2Client.category_option_groups` — CRUD + membership helpers."""
def __init__(self, client: Dhis2Client) -> None:
"""Bind to the sharing client."""
self._client = client
async def list_all(self) -> list[CategoryOptionGroup]:
"""Return every CategoryOptionGroup."""
return cast(
list[CategoryOptionGroup],
await self._client.resources.category_option_groups.list(
fields=_COG_FIELDS,
paging=False,
),
)
async def get(self, uid: str) -> CategoryOptionGroup:
"""Fetch one group by UID with `categoryOptions` + `groupSets` populated."""
return await self._client.get(
f"/api/categoryOptionGroups/{uid}", model=CategoryOptionGroup, params={"fields": _COG_FIELDS}
)
async def list_members(
self,
uid: str,
*,
page: int = 1,
page_size: int = 50,
) -> list[CategoryOption]:
"""Page through CategoryOptions belonging to one group."""
return cast(
list[CategoryOption],
await self._client.resources.category_options.list(
fields=_MEMBER_FIELDS,
filters=[f"categoryOptionGroups.id:eq:{uid}"],
order=["name:asc"],
page=page,
page_size=page_size,
),
)
async def create(
self,
*,
name: str,
short_name: str,
data_dimension_type: str = "DISAGGREGATION",
uid: str | None = None,
code: str | None = None,
description: str | None = None,
) -> CategoryOptionGroup:
"""Create an empty group; add members afterwards via `add_members`.
`data_dimension_type=DISAGGREGATION` (default) is the common case;
`ATTRIBUTE` is the other value DHIS2 accepts — used when the group
targets the attribute-combo grid (data source / funder / etc.)
instead of the disaggregation grid (sex / age / etc.).
"""
payload: dict[str, Any] = {
"name": name,
"shortName": short_name,
"dataDimensionType": data_dimension_type,
}
if uid:
payload["id"] = uid
if code:
payload["code"] = code
if description:
payload["description"] = description
envelope = await self._client.post("/api/categoryOptionGroups", payload, model=WebMessageResponse)
created_uid = envelope.created_uid or uid
if not created_uid:
raise RuntimeError("category-option-group create did not return a uid")
return await self.get(created_uid)
async def update(self, group: CategoryOptionGroup) -> CategoryOptionGroup:
"""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/categoryOptionGroups/{group.id}", body=body)
return await self.get(group.id)
async def add_members(self, uid: str, *, category_option_uids: list[str]) -> CategoryOptionGroup:
"""Add CategoryOptions to the group via the per-item POST shortcut."""
for co_uid in category_option_uids:
await self._client.resources.category_option_groups.add_collection_item(uid, "categoryOptions", co_uid)
return await self.get(uid)
async def remove_members(
self,
uid: str,
*,
category_option_uids: list[str],
) -> CategoryOptionGroup:
"""Drop CategoryOptions from the group via the per-item DELETE shortcut."""
for co_uid in category_option_uids:
await self._client.resources.category_option_groups.remove_collection_item(uid, "categoryOptions", co_uid)
return await self.get(uid)
async def delete(self, uid: str) -> None:
"""Delete the grouping row — member category options stay."""
if not uid:
raise ValueError("delete requires a non-empty uid")
await self._client.resources.category_option_groups.delete(uid)
|
Functions
__init__(client)
Bind to the sharing client.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| def __init__(self, client: Dhis2Client) -> None:
"""Bind to the sharing client."""
self._client = client
|
list_all()
async
Return every CategoryOptionGroup.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| async def list_all(self) -> list[CategoryOptionGroup]:
"""Return every CategoryOptionGroup."""
return cast(
list[CategoryOptionGroup],
await self._client.resources.category_option_groups.list(
fields=_COG_FIELDS,
paging=False,
),
)
|
get(uid)
async
Fetch one group by UID with categoryOptions + groupSets populated.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| async def get(self, uid: str) -> CategoryOptionGroup:
"""Fetch one group by UID with `categoryOptions` + `groupSets` populated."""
return await self._client.get(
f"/api/categoryOptionGroups/{uid}", model=CategoryOptionGroup, params={"fields": _COG_FIELDS}
)
|
list_members(uid, *, page=1, page_size=50)
async
Page through CategoryOptions belonging to one group.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| async def list_members(
self,
uid: str,
*,
page: int = 1,
page_size: int = 50,
) -> list[CategoryOption]:
"""Page through CategoryOptions belonging to one group."""
return cast(
list[CategoryOption],
await self._client.resources.category_options.list(
fields=_MEMBER_FIELDS,
filters=[f"categoryOptionGroups.id:eq:{uid}"],
order=["name:asc"],
page=page,
page_size=page_size,
),
)
|
create(*, name, short_name, data_dimension_type='DISAGGREGATION', uid=None, code=None, description=None)
async
Create an empty group; add members afterwards via add_members.
data_dimension_type=DISAGGREGATION (default) is the common case;
ATTRIBUTE is the other value DHIS2 accepts — used when the group
targets the attribute-combo grid (data source / funder / etc.)
instead of the disaggregation grid (sex / age / etc.).
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| async def create(
self,
*,
name: str,
short_name: str,
data_dimension_type: str = "DISAGGREGATION",
uid: str | None = None,
code: str | None = None,
description: str | None = None,
) -> CategoryOptionGroup:
"""Create an empty group; add members afterwards via `add_members`.
`data_dimension_type=DISAGGREGATION` (default) is the common case;
`ATTRIBUTE` is the other value DHIS2 accepts — used when the group
targets the attribute-combo grid (data source / funder / etc.)
instead of the disaggregation grid (sex / age / etc.).
"""
payload: dict[str, Any] = {
"name": name,
"shortName": short_name,
"dataDimensionType": data_dimension_type,
}
if uid:
payload["id"] = uid
if code:
payload["code"] = code
if description:
payload["description"] = description
envelope = await self._client.post("/api/categoryOptionGroups", payload, model=WebMessageResponse)
created_uid = envelope.created_uid or uid
if not created_uid:
raise RuntimeError("category-option-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/category_option_groups.py
| async def update(self, group: CategoryOptionGroup) -> CategoryOptionGroup:
"""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/categoryOptionGroups/{group.id}", body=body)
return await self.get(group.id)
|
add_members(uid, *, category_option_uids)
async
Add CategoryOptions to the group via the per-item POST shortcut.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| async def add_members(self, uid: str, *, category_option_uids: list[str]) -> CategoryOptionGroup:
"""Add CategoryOptions to the group via the per-item POST shortcut."""
for co_uid in category_option_uids:
await self._client.resources.category_option_groups.add_collection_item(uid, "categoryOptions", co_uid)
return await self.get(uid)
|
remove_members(uid, *, category_option_uids)
async
Drop CategoryOptions from the group via the per-item DELETE shortcut.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| async def remove_members(
self,
uid: str,
*,
category_option_uids: list[str],
) -> CategoryOptionGroup:
"""Drop CategoryOptions from the group via the per-item DELETE shortcut."""
for co_uid in category_option_uids:
await self._client.resources.category_option_groups.remove_collection_item(uid, "categoryOptions", co_uid)
return await self.get(uid)
|
delete(uid)
async
Delete the grouping row — member category options stay.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_groups.py
| async def delete(self, uid: str) -> None:
"""Delete the grouping row — member category options stay."""
if not uid:
raise ValueError("delete requires a non-empty uid")
await self._client.resources.category_option_groups.delete(uid)
|
category_option_group_sets
CategoryOptionGroupSet authoring — Dhis2Client.category_option_group_sets.
A CategoryOptionGroupSet is the analytics dimension that collects
CategoryOptionGroups — e.g. "Programme funder" carries groups for
each disaggregated donor. Mirrors DataElementGroupSetsAccessor.
Classes
CategoryOptionGroupSet
Bases: BaseModel
Generated model for DHIS2 CategoryOptionGroupSet.
DHIS2 Category Option Group Set - persisted metadata (generated from /api/schemas at DHIS2 v42).
API endpoint: /api/categoryOptionGroupSets.
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_option_group_set.py
| class CategoryOptionGroupSet(BaseModel):
"""Generated model for DHIS2 `CategoryOptionGroupSet`.
DHIS2 Category Option Group Set - persisted metadata (generated from /api/schemas at DHIS2 v42).
API endpoint: /api/categoryOptionGroupSets.
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.")
categoryOptionGroups: list[Any] | None = Field(default=None, description="Collection of CategoryOptionGroup.")
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.")
|
CategoryOptionGroupSetsAccessor
Dhis2Client.category_option_group_sets — CRUD + group-membership helpers.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_group_sets.py
| class CategoryOptionGroupSetsAccessor:
"""`Dhis2Client.category_option_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[CategoryOptionGroupSet]:
"""Return every CategoryOptionGroupSet."""
return cast(
list[CategoryOptionGroupSet],
await self._client.resources.category_option_group_sets.list(
fields=_COGS_FIELDS,
paging=False,
),
)
async def get(self, uid: str) -> CategoryOptionGroupSet:
"""Fetch one group set by UID with its groups inline."""
return await self._client.get(
f"/api/categoryOptionGroupSets/{uid}", model=CategoryOptionGroupSet, params={"fields": _COGS_FIELDS}
)
async def list_groups(self, uid: str) -> list[CategoryOptionGroup]:
"""Return the groups in the set, in definition order."""
group_set = await self.get(uid)
groups = group_set.categoryOptionGroups or []
return [CategoryOptionGroup.model_validate(g) for g in groups if isinstance(g, dict)]
async def create(
self,
*,
name: str,
short_name: str,
data_dimension_type: str = "DISAGGREGATION",
data_dimension: bool = True,
uid: str | None = None,
code: str | None = None,
description: str | None = None,
) -> CategoryOptionGroupSet:
"""Create an empty group set; add groups via `add_groups`.
`data_dimension=True` (the default) exposes the set as an
analytics axis (pivot tables, visualisations).
`data_dimension_type="DISAGGREGATION"` targets the disaggregation
grid; `"ATTRIBUTE"` targets the attribute-combo grid (data
source / funder / etc.).
"""
payload: dict[str, Any] = {
"name": name,
"shortName": short_name,
"dataDimensionType": data_dimension_type,
"dataDimension": data_dimension,
}
if uid:
payload["id"] = uid
if code:
payload["code"] = code
if description:
payload["description"] = description
envelope = await self._client.post("/api/categoryOptionGroupSets", payload, model=WebMessageResponse)
created_uid = envelope.created_uid or uid
if not created_uid:
raise RuntimeError("category-option-group-set create did not return a uid")
return await self.get(created_uid)
async def update(self, group_set: CategoryOptionGroupSet) -> CategoryOptionGroupSet:
"""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/categoryOptionGroupSets/{group_set.id}", body=body)
return await self.get(group_set.id)
async def add_groups(self, uid: str, *, group_uids: list[str]) -> CategoryOptionGroupSet:
"""Add `group_uids` to the set via the per-item POST shortcut."""
for group_uid in group_uids:
await self._client.resources.category_option_group_sets.add_collection_item(
uid, "categoryOptionGroups", group_uid
)
return await self.get(uid)
async def remove_groups(self, uid: str, *, group_uids: list[str]) -> CategoryOptionGroupSet:
"""Drop `group_uids` from the set via the per-item DELETE shortcut."""
for group_uid in group_uids:
await self._client.resources.category_option_group_sets.remove_collection_item(
uid, "categoryOptionGroups", group_uid
)
return await self.get(uid)
async def delete(self, uid: str) -> None:
"""Delete a group set — groups stay."""
if not uid:
raise ValueError("delete requires a non-empty uid")
await self._client.resources.category_option_group_sets.delete(uid)
|
Functions
__init__(client)
Bind to the sharing client.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_group_sets.py
| def __init__(self, client: Dhis2Client) -> None:
"""Bind to the sharing client."""
self._client = client
|
list_all()
async
Return every CategoryOptionGroupSet.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_group_sets.py
| async def list_all(self) -> list[CategoryOptionGroupSet]:
"""Return every CategoryOptionGroupSet."""
return cast(
list[CategoryOptionGroupSet],
await self._client.resources.category_option_group_sets.list(
fields=_COGS_FIELDS,
paging=False,
),
)
|
get(uid)
async
Fetch one group set by UID with its groups inline.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_group_sets.py
| async def get(self, uid: str) -> CategoryOptionGroupSet:
"""Fetch one group set by UID with its groups inline."""
return await self._client.get(
f"/api/categoryOptionGroupSets/{uid}", model=CategoryOptionGroupSet, params={"fields": _COGS_FIELDS}
)
|
list_groups(uid)
async
Return the groups in the set, in definition order.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_group_sets.py
| async def list_groups(self, uid: str) -> list[CategoryOptionGroup]:
"""Return the groups in the set, in definition order."""
group_set = await self.get(uid)
groups = group_set.categoryOptionGroups or []
return [CategoryOptionGroup.model_validate(g) for g in groups if isinstance(g, dict)]
|
create(*, name, short_name, data_dimension_type='DISAGGREGATION', data_dimension=True, uid=None, code=None, description=None)
async
Create an empty group set; add groups via add_groups.
data_dimension=True (the default) exposes the set as an
analytics axis (pivot tables, visualisations).
data_dimension_type="DISAGGREGATION" targets the disaggregation
grid; "ATTRIBUTE" targets the attribute-combo grid (data
source / funder / etc.).
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_group_sets.py
| async def create(
self,
*,
name: str,
short_name: str,
data_dimension_type: str = "DISAGGREGATION",
data_dimension: bool = True,
uid: str | None = None,
code: str | None = None,
description: str | None = None,
) -> CategoryOptionGroupSet:
"""Create an empty group set; add groups via `add_groups`.
`data_dimension=True` (the default) exposes the set as an
analytics axis (pivot tables, visualisations).
`data_dimension_type="DISAGGREGATION"` targets the disaggregation
grid; `"ATTRIBUTE"` targets the attribute-combo grid (data
source / funder / etc.).
"""
payload: dict[str, Any] = {
"name": name,
"shortName": short_name,
"dataDimensionType": data_dimension_type,
"dataDimension": data_dimension,
}
if uid:
payload["id"] = uid
if code:
payload["code"] = code
if description:
payload["description"] = description
envelope = await self._client.post("/api/categoryOptionGroupSets", payload, model=WebMessageResponse)
created_uid = envelope.created_uid or uid
if not created_uid:
raise RuntimeError("category-option-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/category_option_group_sets.py
| async def update(self, group_set: CategoryOptionGroupSet) -> CategoryOptionGroupSet:
"""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/categoryOptionGroupSets/{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/category_option_group_sets.py
| async def add_groups(self, uid: str, *, group_uids: list[str]) -> CategoryOptionGroupSet:
"""Add `group_uids` to the set via the per-item POST shortcut."""
for group_uid in group_uids:
await self._client.resources.category_option_group_sets.add_collection_item(
uid, "categoryOptionGroups", 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/category_option_group_sets.py
| async def remove_groups(self, uid: str, *, group_uids: list[str]) -> CategoryOptionGroupSet:
"""Drop `group_uids` from the set via the per-item DELETE shortcut."""
for group_uid in group_uids:
await self._client.resources.category_option_group_sets.remove_collection_item(
uid, "categoryOptionGroups", group_uid
)
return await self.get(uid)
|
delete(uid)
async
Delete a group set — groups stay.
Source code in packages/dhis2w-client/src/dhis2w_client/category_option_group_sets.py
| async def delete(self, uid: str) -> None:
"""Delete a group set — groups stay."""
if not uid:
raise ValueError("delete requires a non-empty uid")
await self._client.resources.category_option_group_sets.delete(uid)
|