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, user, userAccesses[], userGroupAccesses[]).
    # DHIS2 defines no `externalAccess` on `SharingObject` on any supported major:
    # the field is absent from the OpenAPI document, and a write carrying it answers
    # 200 "Access control set" while discarding the value (BUGS.md #38).

    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 409 Conflict with Type <name> is not supported. for anything the sharing endpoint will not serve. Status and message are identical whether the type does not exist at all or exists but has no sharing, so the response alone does not tell the two apart. Check /api/schemas/{type} to find out which you hit: a 404 means the name is wrong (the sharing endpoint uses the same singular names as the schema), and a 200 carrying "shareable": false means the type is real but has no sharing block — organisationUnit is the one people try most often.

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/v43/oas/sharing.py
class Sharing(_BaseModel):
    """OpenAPI schema `Sharing`."""

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

    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/v43/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
    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.

DHIS2 defines no externalAccess on SharingObject: the field is absent from the OpenAPI document on every supported major, and a write that carries it answers 200 "Access control set" while discarding the value. The builder exposes no external_access knob and the materialised wire shape names no externalAccess (BUGS.md #38).

Source code in packages/dhis2w-client/src/dhis2w_client/v43/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.

    DHIS2 defines no `externalAccess` on `SharingObject`: the field is absent
    from the OpenAPI document on every supported major, and a write that
    carries it answers 200 `"Access control set"` while discarding the value.
    The builder exposes no `external_access` knob and the materialised wire
    shape names no `externalAccess` (BUGS.md #38).
    """

    model_config = ConfigDict(extra="allow")

    public_access: str = ACCESS_READ_METADATA
    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,
            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()
            ],
        )
Methods:
grant_user(user_id, access)

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

Source code in packages/dhis2w-client/src/dhis2w_client/v43/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/v43/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/v43/sharing.py
def to_sharing_object(self) -> SharingObject:
    """Materialise the builder into the `SharingObject` wire shape."""
    return SharingObject(
        publicAccess=self.public_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/v43/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/v43/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/v43/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,
    )