Skip to content

Sharing helpers

Typed helpers over /api/sharing. Use them to read and replace the sharing block of any DHIS2 object without hand-writing JSON Patch.

Access strings

DHIS2 packs four capabilities into an 8-char string:

Positions Meaning
0–1 metadata read / write — rw, r-, --
2–3 data read / write (only meaningful on dataValue-carrying objects)
4–7 reserved; always ----
from dhis2w_client import access_string, ACCESS_READ_METADATA, ACCESS_READ_WRITE_DATA

access_string()                                 # '--------'
access_string(metadata="rw")                    # 'rw------'
access_string(metadata="r-", data="r-")         # 'r-r-----'
ACCESS_READ_METADATA                            # 'r-------'
ACCESS_READ_WRITE_DATA                          # 'rwrw----'

The four constants cover >95% of callsites; access_string() handles the rest without concatenation at the use site.

Get / apply

from dhis2w_client import Dhis2Client, SharingBuilder, apply_sharing, get_sharing

async with Dhis2Client(url, auth) as client:
    current = await get_sharing(client, "dataSet", ds_uid)
    # current is a `SharingObject` (publicAccess, externalAccess, user, userAccesses[], userGroupAccesses[]).

    sharing = (
        SharingBuilder(owner_user_id=admin_uid)
        .grant_user(admin_uid, "rwrw----")
        .grant_user_group(group_uid, "r-------")
    )
    envelope = await apply_sharing(client, "dataSet", ds_uid, sharing)
    assert envelope.status == "OK"

apply_sharing accepts either a SharingBuilder or a raw SharingObject (use the builder for ergonomics, drop down to the object when you already have one from get_sharing).

Resource types

resource_type is DHIS2's singular metadata name as it appears in filter strings and in the sharing API's ?type= param. Common values:

  • dataSet, dataElement, categoryCombo, categoryOption
  • program, programStage, trackedEntityType
  • user, userGroup, userRole
  • dashboard, visualization, map, report, eventChart, eventReport

DHIS2 returns 400 on unknown types — if you get that, check the resource's schema at /api/schemas/{type} (the sharing endpoint uses the same names).

Why this exists

Before this helper, callers typically reached for JSON Patch against /api/<resource>/<uid>:

# Old pattern — works but verbose, error-prone, untyped.
await client.patch_raw(
    f"/api/dataSets/{uid}",
    [{"op": "add", "path": "/sharing/users", "value": {admin_uid: {"id": admin_uid, "access": "rwrw----"}}}],
)

The typed helper replaces that with one line, and get_sharing + builder composition makes additive edits (preserve everything, append one grant) straightforward. See examples/client/bootstrap_zero_to_data.py step 5 for the before/after.

sharing

Typed helpers for DHIS2's sharing API.

DHIS2 models access to every persistable object as a sharing block: a public-access string, an owning user, and two lists of per-user / per-user-group grants. The wire shape has two flavours:

  • SharingObject — what /api/sharing?type=<t>&id=<uid> returns inside SharingInfo.object. Carries publicAccess, userAccesses, userGroupAccesses.
  • Sharing — what metadata resources carry on their sharing field. Same information but userAccesses[] becomes a keyed map users: {uid: {...}} (same for groups), matching how DHIS2 stores it internally.

This module ships three utilities:

  • access_string(metadata="rw", data="rw") — compose the 8-char access string DHIS2 uses ("rwrw----", "r-------", etc.). Constants for the common forms.
  • get_sharing(client, resource_type, uid) — fetch the current block via GET /api/sharing.
  • apply_sharing(client, resource_type, uid, sharing) — replace it via POST /api/sharing. Returns a WebMessageResponse.
  • A SharingBuilder convenience for building up SharingObject without typing the map-vs-list details by hand.

Classes

Sharing

Bases: BaseModel

OpenAPI schema Sharing.

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

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

    external: bool | None = None
    owner: str | None = None
    public: str | None = None
    userGroups: dict[str, UserGroupAccess] | None = None
    users: dict[str, UserAccess] | None = None

SharingObject

Bases: BaseModel

OpenAPI schema SharingObject.

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

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

    displayName: str | None = None
    externalAccess: bool | None = None
    id: str | None = None
    name: str | None = None
    publicAccess: str | None = None
    user: SharingUser | None = None
    userAccesses: list[SharingUserAccess] | None = None
    userGroupAccesses: list[SharingUserGroupAccess] | None = None

SharingBuilder

Bases: BaseModel

Fluent-ish builder for SharingObject so callers don't hand-assemble maps.

Callers that want to attach fresh sharing to an object work in terms of "grant user X read+write", not "build a SharingUserAccess and append it to the list". The builder hides that boilerplate while producing the exact wire shape POST /api/sharing wants.

Source code in packages/dhis2w-client/src/dhis2w_client/sharing.py
class SharingBuilder(BaseModel):
    """Fluent-ish builder for `SharingObject` so callers don't hand-assemble maps.

    Callers that want to attach fresh sharing to an object work in terms of
    "grant user X read+write", not "build a `SharingUserAccess` and append it
    to the list". The builder hides that boilerplate while producing the exact
    wire shape `POST /api/sharing` wants.
    """

    model_config = ConfigDict(extra="allow")

    public_access: str = ACCESS_READ_METADATA
    external_access: bool = False
    owner_user_id: str | None = None
    user_accesses: dict[str, str] = {}
    user_group_accesses: dict[str, str] = {}

    def grant_user(self, user_id: str, access: str) -> SharingBuilder:
        """Grant `access` to one user (overwrites any existing grant)."""
        return self.model_copy(update={"user_accesses": {**self.user_accesses, user_id: access}})

    def grant_user_group(self, group_id: str, access: str) -> SharingBuilder:
        """Grant `access` to one user group (overwrites any existing grant)."""
        return self.model_copy(
            update={"user_group_accesses": {**self.user_group_accesses, group_id: access}},
        )

    def to_sharing_object(self) -> SharingObject:
        """Materialise the builder into the `SharingObject` wire shape."""
        return SharingObject(
            publicAccess=self.public_access,
            externalAccess=self.external_access,
            user=SharingUser(id=self.owner_user_id) if self.owner_user_id else None,
            userAccesses=[SharingUserAccess(id=uid, access=access) for uid, access in self.user_accesses.items()],
            userGroupAccesses=[
                SharingUserGroupAccess(id=gid, access=access) for gid, access in self.user_group_accesses.items()
            ],
        )
Functions
grant_user(user_id, access)

Grant access to one user (overwrites any existing grant).

Source code in packages/dhis2w-client/src/dhis2w_client/sharing.py
def grant_user(self, user_id: str, access: str) -> SharingBuilder:
    """Grant `access` to one user (overwrites any existing grant)."""
    return self.model_copy(update={"user_accesses": {**self.user_accesses, user_id: access}})
grant_user_group(group_id, access)

Grant access to one user group (overwrites any existing grant).

Source code in packages/dhis2w-client/src/dhis2w_client/sharing.py
def grant_user_group(self, group_id: str, access: str) -> SharingBuilder:
    """Grant `access` to one user group (overwrites any existing grant)."""
    return self.model_copy(
        update={"user_group_accesses": {**self.user_group_accesses, group_id: access}},
    )
to_sharing_object()

Materialise the builder into the SharingObject wire shape.

Source code in packages/dhis2w-client/src/dhis2w_client/sharing.py
def to_sharing_object(self) -> SharingObject:
    """Materialise the builder into the `SharingObject` wire shape."""
    return SharingObject(
        publicAccess=self.public_access,
        externalAccess=self.external_access,
        user=SharingUser(id=self.owner_user_id) if self.owner_user_id else None,
        userAccesses=[SharingUserAccess(id=uid, access=access) for uid, access in self.user_accesses.items()],
        userGroupAccesses=[
            SharingUserGroupAccess(id=gid, access=access) for gid, access in self.user_group_accesses.items()
        ],
    )

Functions

access_string(*, metadata='--', data='--')

Compose an 8-char DHIS2 access string from metadata + data r/w pairs.

Examples:

>>> access_string(metadata="rw")  # metadata-only read/write
'rw------'
>>> access_string(metadata="r-", data="r-")  # read-everything
'r-r-----'
>>> access_string(metadata="rw", data="rw")  # full access
'rwrw----'
Source code in packages/dhis2w-client/src/dhis2w_client/sharing.py
def access_string(*, metadata: AccessPattern = "--", data: AccessPattern = "--") -> str:
    """Compose an 8-char DHIS2 access string from metadata + data r/w pairs.

    Examples:
        >>> access_string(metadata="rw")  # metadata-only read/write
        'rw------'
        >>> access_string(metadata="r-", data="r-")  # read-everything
        'r-r-----'
        >>> access_string(metadata="rw", data="rw")  # full access
        'rwrw----'
    """
    metadata_part = metadata.ljust(2, "-")
    data_part = data.ljust(2, "-")
    return f"{metadata_part}{data_part}----"

get_sharing(client, resource_type, uid) async

Fetch GET /api/sharing?type=<resource_type>&id=<uid> → SharingObject.

resource_type is the DHIS2 singular metadata-resource name as it appears in filter syntax and in the sharing API's ?type= param — e.g. "dataSet", "dataElement", "program", "user". DHIS2 returns a {meta, object} envelope; callers almost always want just .object.

Source code in packages/dhis2w-client/src/dhis2w_client/sharing.py
async def get_sharing(client: Dhis2Client, resource_type: str, uid: str) -> SharingObject:
    """Fetch `GET /api/sharing?type=<resource_type>&id=<uid>` → SharingObject.

    `resource_type` is the DHIS2 singular metadata-resource name as it appears
    in filter syntax and in the sharing API's `?type=` param — e.g.
    `"dataSet"`, `"dataElement"`, `"program"`, `"user"`. DHIS2 returns a
    `{meta, object}` envelope; callers almost always want just `.object`.
    """
    raw = await client.get_raw("/api/sharing", params={"type": resource_type, "id": uid})
    info = SharingInfo.model_validate(raw)
    if info.object is None:
        raise ValueError(f"/api/sharing returned no `object` for {resource_type}/{uid}")
    return info.object

apply_sharing(client, resource_type, uid, sharing) async

Replace the sharing block via POST /api/sharing?type=<t>&id=<uid>.

DHIS2 ignores unknown fields and preserves the owner user when the payload omits it. Accepts either a SharingObject (raw wire shape) or a SharingBuilder (convenience form).

Source code in packages/dhis2w-client/src/dhis2w_client/sharing.py
async def apply_sharing(
    client: Dhis2Client,
    resource_type: str,
    uid: str,
    sharing: SharingObject | SharingBuilder,
) -> WebMessageResponse:
    """Replace the sharing block via `POST /api/sharing?type=<t>&id=<uid>`.

    DHIS2 ignores unknown fields and preserves the owner user when the payload
    omits it. Accepts either a `SharingObject` (raw wire shape) or a
    `SharingBuilder` (convenience form).
    """
    payload_obj = sharing.to_sharing_object() if isinstance(sharing, SharingBuilder) else sharing
    payload = {"object": payload_obj.model_dump(by_alias=True, exclude_none=True, mode="json")}
    return await client.post(
        "/api/sharing",
        payload,
        params={"type": resource_type, "id": uid},
        model=WebMessageResponse,
    )