Metadata CRUD via generated resources¶
Every DHIS2 metadata type exposed on /api/schemas gets a generated _<Name>Resource class with full CRUD. dhis2 codegen stamps them into dhis2w_client/generated/v{NN}/resources.py and binds them to Dhis2Client as client.resources.<attr_name> at connect time.
Surface per resource¶
class _DataElementResource:
_path = "/api/dataElements"
_plural_key = "dataElements"
async def get(self, uid, *, fields=None) -> DataElement: ...
async def list(self, *, fields=None, filter=None, order=None) -> list[DataElement]: ...
async def list_raw(self, *, fields=None, filter=None, order=None, paging=False) -> dict: ...
async def create(self, item: DataElement) -> dict: ...
async def update(self, item: DataElement) -> dict: ...
async def delete(self, uid: str) -> dict: ...
End-to-end usage¶
from dhis2w_client import BasicAuth, Dhis2Client
from dhis2w_client.generated.v42.enums import AggregationType, DataElementDomain, ValueType
from dhis2w_client.generated.v42.schemas.data_element import DataElement, Reference
async with Dhis2Client(
base_url="https://play.im.dhis2.org/dev",
auth=BasicAuth("system", "System123"),
) as client:
# typed list
elements = await client.resources.data_elements.list(fields="id,name")
# typed get
one = await client.resources.data_elements.get("abc123")
# typed create — CONSTANT fields are StrEnums; bare strings also work.
new = DataElement(
id="abc12345678",
name="Test DE",
shortName="Test",
valueType=ValueType.NUMBER,
domainType=DataElementDomain.AGGREGATE,
aggregationType=AggregationType.SUM,
categoryCombo=Reference(id=cc_uid),
)
response = await client.resources.data_elements.create(new)
# typed update — reads item.id for the URL
one.name = "Renamed"
await client.resources.data_elements.update(one)
# delete
await client.resources.data_elements.delete("abc123")
Enum classes live under dhis2w_client.generated.v{N}.enums. Each is a StrEnum so ValueType.NUMBER == "NUMBER" is true and a bare string passed to a pydantic constructor still validates.
list vs list_raw¶
list(...)— parses the response's plural key into typed models, returnslist[Model]. Paging is forced off (single request). Simple, strongly typed.list_raw(..., paging=True)— returns the raw DHIS2 dict including thepagerblock ({"page", "pageSize", "total", "pageCount"}). Use this when you need pager metadata, or when you want to drive your own page loop.
A typed paging helper (list_paged) that yields models across pages will land when a real use-case surfaces. Today, list(paging=False) covers most workflows.
Create/update request bodies¶
Both create and update dump the pydantic model with model_dump(by_alias=True, exclude_none=True). Because generated models use camelCase field names directly (not aliases), this is effectively exclude_none — any field left as None is stripped before POST/PUT. This matches what DHIS2 expects: only send the fields you care about.
update requires item.id to be populated — ValueError is raised otherwise. DHIS2's PUT endpoint is a full replace, not a partial patch; callers should fetch-modify-put rather than PUTting partial payloads.
What's not in the generated surface¶
- PATCH — DHIS2 supports RFC 6902 JSON Patch on some endpoints. We don't generate for it; use
client.put_rawmanually with a patched payload if needed. - Bulk
/api/metadata— that's a multi-type import bundle, not a per-resource operation. It gets a dedicated helper indhis2w-client/metadata_import.py(deferred). /api/metadataGET with schemas mixed into one response — same story.- Sharing (
/api/sharing?type=...) — a separate endpoint that operates across metadata types; deferred. - Tracker — lives at
/api/tracker/*and has its own API shape. Hand-written module. - Data values —
/api/dataValueSetsand/api/dataValues. Hand-written module. - Analytics —
/api/analytics. Hand-written module.
Design choices¶
- Typed
listdefaults topaging=false— simplest mental model, single round trip. If you need pagination, drop tolist_raw. create/updatereturn raw dicts, not parsed models — DHIS2 returns an import-summary payload ({status, stats, response}) that isn't the resource shape. Parsing it into a model would hide detail; leaving it raw is honest.updateraises on missing id — rather than POSTing to a URL without a UID. Catching the bug at the pydantic object level, not silently at HTTP level.model_dump(by_alias=True, exclude_none=True)— camelCase already matches DHIS2's wire format, so by_alias is technically a no-op. Keeping it anyway so if aliases ever appear (e.g. for reserved words), serialisation stays correct.