Skip to content

Files

Document, FileResource, FileResourceDomain, and the FilesAccessor bound to Dhis2Client.files. Wraps DHIS2's two file surfaces — Document metadata (URL or binary) at /api/documents and the upload-and-attach-later FileResource flow at /api/fileResources.

When to reach for it

  • Attach a file to a message (MESSAGE_ATTACHMENT domain) — see Messaging.
  • Carry an external URL on the metadata side as a Document (no upload — DHIS2 just links out).
  • Upload a binary Document (PDF, image, …) so DHIS2 hosts it for the apps tier.
  • Attach an image to a data value (DATA_VALUE domain).

Worked example — external-URL document round-trip

from dhis2w_core.client_context import open_client
from dhis2w_core.profile import profile_from_env

async with open_client(profile_from_env()) as client:
    # 1. Create an external-URL document — no bytes leave the laptop.
    doc = await client.files.create_external_document(
        name="Country health plan 2026",
        url="https://example.org/plan.pdf",
    )
    print(f"created {doc.id}  external={doc.external}")

    # 2. List + filter (server-side DHIS2 filter DSL — `filter` is a single string).
    rows = await client.files.list_documents(filter="external:eq:true", page_size=10)
    for d in rows:
        print(f"  {d.id}  {d.name}  -> {d.url}")

Worked example — binary file-resource upload, then attach

from pathlib import Path

async with open_client(profile_from_env()) as client:
    # 1. Read the bytes + push them to DHIS2's FileResource pool.
    #    `upload_file_resource` takes raw bytes + a filename; domain
    #    defaults to DATA_VALUE — set MESSAGE_ATTACHMENT for messages,
    #    DOCUMENT for documents, USER_AVATAR for profile pictures.
    data = Path("./report.pdf").read_bytes()
    resource = await client.files.upload_file_resource(
        data,
        filename="report.pdf",
        domain="MESSAGE_ATTACHMENT",
    )
    # 2. Reference the UID from the owning object (e.g. a message — see Messaging).
    print(f"resource {resource.id}  ready to attach")

The two-step pattern is DHIS2-imposed: the upload happens once, then the owning metadata object (message, data value, document) carries the resource UID as a normal field.

Worked end-to-end demo: examples/v42/client/files_documents.py.

files

Document management + typed file-resource upload/download.

Two orthogonal DHIS2 file surfaces, one accessor on Dhis2Client.files:

  • /api/documents — user-uploaded attachments tied to a CRUD metadata object. Types: UPLOAD_FILE (binary stored in DHIS2), EXTERNAL_URL (URL the DHIS2 UI links to). Appears in the DHIS2 Data Administration app.
  • /api/fileResources — typed binary blobs referenced from other metadata: DATA_VALUE (file-type DataElement captures), ICON, MESSAGE_ATTACHMENT, etc. Create with a domain, get a UID back, then reference that UID from the owning resource.

Neither is part of the customize plugin — branding logos go through a different endpoint family. This accessor is for operator-owned document management + capture-media pipelines.

Classes

Document

Bases: BaseModel

OpenAPI schema Document.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/oas/document.py
class Document(_BaseModel):
    """OpenAPI schema `Document`."""

    model_config = _ConfigDict(extra="allow", populate_by_name=True, defer_build=True)

    access: Access | None = None
    attachment: bool | None = None
    attributeValues: list[AttributeValue] | None = None
    code: str | None = None
    contentType: str | None = None
    created: datetime | None = None
    createdBy: UserDto | None = None
    displayName: str | None = None
    external: bool | None = None
    favorite: bool | None = None
    favorites: list[str] | None = None
    href: str | None = None
    id: str | None = None
    lastUpdated: datetime | None = None
    lastUpdatedBy: UserDto | None = None
    name: str | None = None
    sharing: Sharing | None = None
    translations: list[Translation] | None = None
    url: str | None = None

FileResource

Bases: BaseModel

OpenAPI schema FileResource.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/oas/file_resource.py
class FileResource(_BaseModel):
    """OpenAPI schema `FileResource`."""

    model_config = _ConfigDict(extra="allow", populate_by_name=True, defer_build=True)

    access: Access | None = None
    assigned: bool | None = None
    attributeValues: list[AttributeValue] | None = None
    code: str | None = None
    contentLength: int | None = None
    contentMd5: str | None = None
    contentType: str | None = None
    created: datetime | None = None
    createdBy: UserDto | None = None
    displayName: str | None = None
    domain: FileResourceDomain | None = None
    favorite: bool | None = None
    favorites: list[str] | None = None
    hasMultipleStorageFiles: bool | None = None
    href: str | None = None
    id: str | None = None
    lastUpdated: datetime | None = None
    lastUpdatedBy: UserDto | None = None
    name: str | None = None
    sharing: Sharing | None = None
    storageStatus: FileResourceStorageStatus | None = None
    translations: list[Translation] | None = None

FileResourceDomain

Bases: StrEnum

FileResourceDomain.

Source code in packages/dhis2w-client/src/dhis2w_client/generated/v42/oas/_enums.py
class FileResourceDomain(StrEnum):
    """FileResourceDomain."""

    DATA_VALUE = "DATA_VALUE"
    PUSH_ANALYSIS = "PUSH_ANALYSIS"
    DOCUMENT = "DOCUMENT"
    MESSAGE_ATTACHMENT = "MESSAGE_ATTACHMENT"
    USER_AVATAR = "USER_AVATAR"
    ORG_UNIT = "ORG_UNIT"
    ICON = "ICON"
    JOB_DATA = "JOB_DATA"

FilesAccessor

Dhis2Client.files — documents + file resources.

All calls reuse the client's open HTTP pool + auth. Downloads buffer the response body into memory (DHIS2 documents + file resources are typically < 10 MB each); stream-download for larger payloads belongs on a follow-up when a real use case surfaces.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
class FilesAccessor:
    """`Dhis2Client.files` — documents + file resources.

    All calls reuse the client's open HTTP pool + auth. Downloads buffer
    the response body into memory (DHIS2 documents + file resources are
    typically < 10 MB each); stream-download for larger payloads belongs
    on a follow-up when a real use case surfaces.
    """

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

    # ---- documents --------------------------------------------------

    async def list_documents(
        self,
        *,
        filter: str | None = None,
        page: int | None = None,
        page_size: int | None = None,
    ) -> list[Document]:
        """List `/api/documents` — typed `Document` objects without the binary payload."""
        filters: list[str] | None = [filter] if filter else None
        return cast(
            list[Document],
            await self._client.resources.documents.list(
                fields=":owner",
                filters=filters,
                page=page,
                page_size=page_size,
            ),
        )

    async def get_document(self, uid: str) -> Document:
        """Return metadata for one document (`/api/documents/{uid}`)."""
        return await self._client.get(f"/api/documents/{uid}", model=Document)

    async def upload_document(
        self,
        data: bytes,
        *,
        name: str,
        filename: str | None = None,
        content_type: str | None = None,
    ) -> Document:
        """Upload a binary as a DHIS2 document and return the created `Document`.

        DHIS2 doesn't accept multipart-form POSTs on `/api/documents`
        directly (415 Unsupported Media Type — see BUGS.md #16). The
        correct workflow is a two-step:

        1. Upload the bytes as a `FileResource` with `domain=DOCUMENT` via
           `/api/fileResources`.
        2. `POST /api/documents` (JSON) with `url=<fileResourceUid>` so the
           document references the already-stored file resource.

        `contentType` is inferred from `filename` when not set. The
        returned `Document` carries the new UID.
        """
        resolved_filename = filename or name
        file_resource = await self.upload_file_resource(
            data,
            filename=resolved_filename,
            domain=FileResourceDomain.DOCUMENT,
            content_type=content_type,
        )
        raw = await self._client.post_raw(
            "/api/documents",
            body={
                "name": name,
                "url": file_resource.id,
                "external": False,
                "attachment": True,
            },
        )
        created_uid = _uid_from_web_message(raw)
        if created_uid is None:
            raise RuntimeError(f"document upload did not return a uid: {raw!r}")
        return await self.get_document(created_uid)

    async def create_external_document(
        self,
        *,
        name: str,
        url: str,
    ) -> Document:
        """Create an EXTERNAL_URL document (no binary; DHIS2 links to `url`)."""
        body = {"name": name, "url": url, "external": True, "attachment": False}
        raw = await self._client.post_raw("/api/documents", body=body)
        created_uid = _uid_from_web_message(raw)
        if created_uid is None:
            raise RuntimeError(f"external document create did not return a uid: {raw!r}")
        return await self.get_document(created_uid)

    async def download_document(self, uid: str) -> bytes:
        """Fetch the binary payload at `/api/documents/{uid}/data`."""
        response = await self._client._request(  # noqa: SLF001
            "GET",
            f"/api/documents/{uid}/data",
        )
        return response.content

    async def delete_document(self, uid: str) -> None:
        """Delete `/api/documents/{uid}`."""
        await self._client.resources.documents.delete(uid)

    # ---- file resources --------------------------------------------

    async def upload_file_resource(
        self,
        data: bytes,
        *,
        filename: str,
        domain: FileResourceDomain | str = FileResourceDomain.DATA_VALUE,
        content_type: str | None = None,
    ) -> FileResource:
        """Upload a file resource and return the created `FileResource`.

        `domain` picks the storage family (`DATA_VALUE` for file-type DE
        captures, `ICON` for custom icons, `MESSAGE_ATTACHMENT`, etc.).
        DHIS2 returns a `WebMessage` envelope with the created UID under
        `response.fileResource.id` (NOT `response.uid` like `/api/documents`).
        The returned `FileResource` is then the UID you reference from
        the owning resource.
        """
        resolved_content_type = content_type or _guess_content_type(filename)
        domain_value = domain.value if isinstance(domain, FileResourceDomain) else str(domain)
        response = await self._client._request(  # noqa: SLF001
            "POST",
            "/api/fileResources",
            files={"file": (filename, data, resolved_content_type)},
            params={"domain": domain_value},
        )
        body = response.json() if response.content else {}
        created_uid = _uid_from_file_resource_response(body)
        if created_uid is None:
            raise RuntimeError(f"fileResource upload did not return a uid: {body!r}")
        return await self.get_file_resource(created_uid)

    async def get_file_resource(self, uid: str) -> FileResource:
        """Return metadata for one file resource (`/api/fileResources/{uid}`)."""
        return await self._client.get(f"/api/fileResources/{uid}", model=FileResource)

    async def download_file_resource(self, uid: str) -> bytes:
        """Fetch the binary payload at `/api/fileResources/{uid}/data`."""
        response = await self._client._request(  # noqa: SLF001
            "GET",
            f"/api/fileResources/{uid}/data",
        )
        return response.content
Functions
__init__(client)

Bind to the sharing client.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
def __init__(self, client: Dhis2Client) -> None:
    """Bind to the sharing client."""
    self._client = client
list_documents(*, filter=None, page=None, page_size=None) async

List /api/documents — typed Document objects without the binary payload.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def list_documents(
    self,
    *,
    filter: str | None = None,
    page: int | None = None,
    page_size: int | None = None,
) -> list[Document]:
    """List `/api/documents` — typed `Document` objects without the binary payload."""
    filters: list[str] | None = [filter] if filter else None
    return cast(
        list[Document],
        await self._client.resources.documents.list(
            fields=":owner",
            filters=filters,
            page=page,
            page_size=page_size,
        ),
    )
get_document(uid) async

Return metadata for one document (/api/documents/{uid}).

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def get_document(self, uid: str) -> Document:
    """Return metadata for one document (`/api/documents/{uid}`)."""
    return await self._client.get(f"/api/documents/{uid}", model=Document)
upload_document(data, *, name, filename=None, content_type=None) async

Upload a binary as a DHIS2 document and return the created Document.

DHIS2 doesn't accept multipart-form POSTs on /api/documents directly (415 Unsupported Media Type — see BUGS.md #16). The correct workflow is a two-step:

  1. Upload the bytes as a FileResource with domain=DOCUMENT via /api/fileResources.
  2. POST /api/documents (JSON) with url=<fileResourceUid> so the document references the already-stored file resource.

contentType is inferred from filename when not set. The returned Document carries the new UID.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def upload_document(
    self,
    data: bytes,
    *,
    name: str,
    filename: str | None = None,
    content_type: str | None = None,
) -> Document:
    """Upload a binary as a DHIS2 document and return the created `Document`.

    DHIS2 doesn't accept multipart-form POSTs on `/api/documents`
    directly (415 Unsupported Media Type — see BUGS.md #16). The
    correct workflow is a two-step:

    1. Upload the bytes as a `FileResource` with `domain=DOCUMENT` via
       `/api/fileResources`.
    2. `POST /api/documents` (JSON) with `url=<fileResourceUid>` so the
       document references the already-stored file resource.

    `contentType` is inferred from `filename` when not set. The
    returned `Document` carries the new UID.
    """
    resolved_filename = filename or name
    file_resource = await self.upload_file_resource(
        data,
        filename=resolved_filename,
        domain=FileResourceDomain.DOCUMENT,
        content_type=content_type,
    )
    raw = await self._client.post_raw(
        "/api/documents",
        body={
            "name": name,
            "url": file_resource.id,
            "external": False,
            "attachment": True,
        },
    )
    created_uid = _uid_from_web_message(raw)
    if created_uid is None:
        raise RuntimeError(f"document upload did not return a uid: {raw!r}")
    return await self.get_document(created_uid)
create_external_document(*, name, url) async

Create an EXTERNAL_URL document (no binary; DHIS2 links to url).

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def create_external_document(
    self,
    *,
    name: str,
    url: str,
) -> Document:
    """Create an EXTERNAL_URL document (no binary; DHIS2 links to `url`)."""
    body = {"name": name, "url": url, "external": True, "attachment": False}
    raw = await self._client.post_raw("/api/documents", body=body)
    created_uid = _uid_from_web_message(raw)
    if created_uid is None:
        raise RuntimeError(f"external document create did not return a uid: {raw!r}")
    return await self.get_document(created_uid)
download_document(uid) async

Fetch the binary payload at /api/documents/{uid}/data.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def download_document(self, uid: str) -> bytes:
    """Fetch the binary payload at `/api/documents/{uid}/data`."""
    response = await self._client._request(  # noqa: SLF001
        "GET",
        f"/api/documents/{uid}/data",
    )
    return response.content
delete_document(uid) async

Delete /api/documents/{uid}.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def delete_document(self, uid: str) -> None:
    """Delete `/api/documents/{uid}`."""
    await self._client.resources.documents.delete(uid)
upload_file_resource(data, *, filename, domain=FileResourceDomain.DATA_VALUE, content_type=None) async

Upload a file resource and return the created FileResource.

domain picks the storage family (DATA_VALUE for file-type DE captures, ICON for custom icons, MESSAGE_ATTACHMENT, etc.). DHIS2 returns a WebMessage envelope with the created UID under response.fileResource.id (NOT response.uid like /api/documents). The returned FileResource is then the UID you reference from the owning resource.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def upload_file_resource(
    self,
    data: bytes,
    *,
    filename: str,
    domain: FileResourceDomain | str = FileResourceDomain.DATA_VALUE,
    content_type: str | None = None,
) -> FileResource:
    """Upload a file resource and return the created `FileResource`.

    `domain` picks the storage family (`DATA_VALUE` for file-type DE
    captures, `ICON` for custom icons, `MESSAGE_ATTACHMENT`, etc.).
    DHIS2 returns a `WebMessage` envelope with the created UID under
    `response.fileResource.id` (NOT `response.uid` like `/api/documents`).
    The returned `FileResource` is then the UID you reference from
    the owning resource.
    """
    resolved_content_type = content_type or _guess_content_type(filename)
    domain_value = domain.value if isinstance(domain, FileResourceDomain) else str(domain)
    response = await self._client._request(  # noqa: SLF001
        "POST",
        "/api/fileResources",
        files={"file": (filename, data, resolved_content_type)},
        params={"domain": domain_value},
    )
    body = response.json() if response.content else {}
    created_uid = _uid_from_file_resource_response(body)
    if created_uid is None:
        raise RuntimeError(f"fileResource upload did not return a uid: {body!r}")
    return await self.get_file_resource(created_uid)
get_file_resource(uid) async

Return metadata for one file resource (/api/fileResources/{uid}).

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def get_file_resource(self, uid: str) -> FileResource:
    """Return metadata for one file resource (`/api/fileResources/{uid}`)."""
    return await self._client.get(f"/api/fileResources/{uid}", model=FileResource)
download_file_resource(uid) async

Fetch the binary payload at /api/fileResources/{uid}/data.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/files.py
async def download_file_resource(self, uid: str) -> bytes:
    """Fetch the binary payload at `/api/fileResources/{uid}/data`."""
    response = await self._client._request(  # noqa: SLF001
        "GET",
        f"/api/fileResources/{uid}/data",
    )
    return response.content