Skip to content

Profile + lightweight open_client

The Profile Pydantic model + open_client(profile) for PAT and Basic auth live in dhis2w-client — no dhis2w-core install needed for library users embedding the client in their own Python tooling. OAuth2 still requires dhis2w-core because OAuth2 token refresh needs the concurrent-writer-safe token store; calling dhis2w_client.open_client(oauth2_profile) raises NotImplementedError pointing at dhis2w_core.open_client.

When to reach for this surface

  • Embedding dhis2w-client in a third-party app (FastAPI service, script, notebook) with PAT or Basic auth — you don't want the full CLI/MCP runtime weight (Typer, FastMCP, SQLAlchemy, bcrypt, questionary).
  • OAuth2 auth? Use dhis2w_core.open_client(profile) instead — same Profile model, full token-store-backed refresh.
  • Multi-profile TOML resolution? That lives in dhis2w-core. Use dhis2w_core.profile_from_env() for the full precedence chain (TOML + env), or this module's profile_from_env_raw() for the env-only fallback that returns None instead of consulting TOML.

Worked example — pure dhis2w-client use

import asyncio

from dhis2w_client import NoProfileError, Profile, open_client, profile_from_env_raw


async def main() -> None:
    """Build a Profile (in-memory or from env) and open a connected client."""
    profile = profile_from_env_raw()
    if profile is None:
        # No DHIS2_URL+credentials env — construct one explicitly:
        profile = Profile(
            base_url="https://play.im.dhis2.org/dev-2-43",
            auth="pat",
            token="d2pat_yourtoken",
        )
    try:
        async with open_client(profile) as client:
            me = await client.system.me()
            info = await client.system.info()
            print(f"Connected to DHIS2 {info.version} as {me.username}")
    except NoProfileError as exc:
        print(f"misconfigured: {exc}")


asyncio.run(main())

See examples/v{41,42,43}/client/profile_pat_pure_client.py for the runnable, version-pinned form.

Profile model

Profile

Bases: BaseModel

Resolved DHIS2 connection settings for a single session.

version is a plugin-tree hint, NOT a wire-client pin. When set, CLI and MCP bootstraps load the matching dhis2w_core.v{N}.plugins.* tree (so v43 plugin overrides for BUGS #33/#34/#35 are picked up against a v43 stack). The wire Dhis2Client always auto-detects the server's version on connect and rebinds accessors via _dispatch.pyprofile.version doesn't override that. When unset, plugin discovery falls back to DHIS2_VERSION env var (41/42/43), then to v42.

Source code in packages/dhis2w-client/src/dhis2w_client/profile.py
class Profile(BaseModel):
    """Resolved DHIS2 connection settings for a single session.

    `version` is a plugin-tree hint, NOT a wire-client pin. When set, CLI
    and MCP bootstraps load the matching `dhis2w_core.v{N}.plugins.*` tree
    (so v43 plugin overrides for BUGS #33/#34/#35 are picked up against a
    v43 stack). The wire `Dhis2Client` always auto-detects the server's
    version on connect and rebinds accessors via `_dispatch.py` —
    `profile.version` doesn't override that. When unset, plugin discovery
    falls back to `DHIS2_VERSION` env var (`41`/`42`/`43`), then to v42.
    """

    model_config = ConfigDict(frozen=True)

    base_url: str
    auth: Literal["pat", "basic", "oauth2"]
    token: str | None = None
    username: str | None = None
    password: str | None = None
    client_id: str | None = None
    client_secret: str | None = None
    scope: str | None = None
    redirect_uri: str | None = None
    version: Dhis2 | None = None
    """Plugin-tree hint (see class docstring). Wire version is auto-detected on connect()."""

Attributes

version = None class-attribute instance-attribute

Plugin-tree hint (see class docstring). Wire version is auto-detected on connect().

Exceptions

NoProfileError

Bases: RuntimeError

Raised when no DHIS2 profile can be resolved.

Source code in packages/dhis2w-client/src/dhis2w_client/profile.py
class NoProfileError(RuntimeError):
    """Raised when no DHIS2 profile can be resolved."""

UnknownProfileError

Bases: LookupError

Raised when a named profile is requested but does not exist in any profile file.

Source code in packages/dhis2w-client/src/dhis2w_client/profile.py
class UnknownProfileError(LookupError):
    """Raised when a named profile is requested but does not exist in any profile file."""

InvalidProfileNameError

Bases: ValueError

Raised when a profile name does not match the required format.

Source code in packages/dhis2w-client/src/dhis2w_client/profile.py
class InvalidProfileNameError(ValueError):
    """Raised when a profile name does not match the required format."""

Constructors + helpers

profile_from_env_raw()

Build a Profile from DHIS2_URL + credentials env vars.

Returns None when DHIS2_URL is unset or no credential pair is present. Recognises DHIS2_PAT (PAT auth, wins over Basic) and the DHIS2_USERNAME + DHIS2_PASSWORD pair (Basic auth). Reads DHIS2_VERSION ("41" / "42" / "43" or "v41" / "v42" / "v43") into Profile.version when set.

Library callers that want full TOML + env precedence resolution (the chain the d2w CLI uses) should install dhis2w-core and call dhis2w_core.profile_from_env() instead.

Source code in packages/dhis2w-client/src/dhis2w_client/profile.py
def profile_from_env_raw() -> Profile | None:
    """Build a `Profile` from `DHIS2_URL` + credentials env vars.

    Returns `None` when `DHIS2_URL` is unset or no credential pair is present.
    Recognises `DHIS2_PAT` (PAT auth, wins over Basic) and the
    `DHIS2_USERNAME` + `DHIS2_PASSWORD` pair (Basic auth). Reads
    `DHIS2_VERSION` (`"41"` / `"42"` / `"43"` or `"v41"` / `"v42"` / `"v43"`)
    into `Profile.version` when set.

    Library callers that want full TOML + env precedence resolution (the
    chain the `d2w` CLI uses) should install `dhis2w-core` and call
    `dhis2w_core.profile_from_env()` instead.
    """
    base_url = os.environ.get("DHIS2_URL", "").rstrip("/")
    if not base_url:
        return None
    version = _env_version()
    pat = os.environ.get("DHIS2_PAT")
    if pat:
        return Profile(base_url=base_url, auth="pat", token=pat, version=version)
    username = os.environ.get("DHIS2_USERNAME")
    password = os.environ.get("DHIS2_PASSWORD")
    if username and password:
        return Profile(base_url=base_url, auth="basic", username=username, password=password, version=version)
    return None

validate_profile_name(name)

Validate and return a profile name.

Rules
  • must not be empty
  • first character must be an ASCII letter (A-Z, a-z)
  • remaining characters must be letters, digits, or underscore
  • max length 64 characters

Typical valid names: local, prod, prod_eu, test42, laohis42. Raises InvalidProfileNameError on violation. The constraint keeps names safe as env var suffixes, TOML keys, and unquoted shell arguments.

Source code in packages/dhis2w-client/src/dhis2w_client/profile.py
def validate_profile_name(name: str) -> str:
    """Validate and return a profile name.

    Rules:
      - must not be empty
      - first character must be an ASCII letter (A-Z, a-z)
      - remaining characters must be letters, digits, or underscore
      - max length 64 characters

    Typical valid names: `local`, `prod`, `prod_eu`, `test42`, `laohis42`.
    Raises `InvalidProfileNameError` on violation. The constraint keeps names
    safe as env var suffixes, TOML keys, and unquoted shell arguments.
    """
    if not name:
        raise InvalidProfileNameError("profile name must be a non-empty string")
    if len(name) > PROFILE_NAME_MAX_LEN:
        raise InvalidProfileNameError(f"profile name {name!r} exceeds the {PROFILE_NAME_MAX_LEN}-character limit")
    if not _PROFILE_NAME_RE.match(name):
        raise InvalidProfileNameError(
            f"profile name {name!r} is invalid — must start with a letter "
            "and contain only letters, digits, and underscores (a-z, A-Z, 0-9, _)"
        )
    return name

Open a client from a profile

build_auth_for_basic(profile) returns a PatAuth or BasicAuth AuthProvider. open_client(profile) is the async context manager that wires that auth provider into a connected Dhis2Client. Both live at the top of dhis2w_client (re-exported from dhis2w_client.v42.client_context) and on each per-version surface (dhis2w_client.v41, dhis2w_client.v42, dhis2w_client.v43).

build_auth_for_basic(profile)

Return a PatAuth or BasicAuth provider for the profile.

Raises NotImplementedError on profile.auth == "oauth2" pointing at dhis2w_core — OAuth2 needs the token-store machinery that lives there. Raises ValueError when the matching credential fields are missing.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/client_context.py
def build_auth_for_basic(profile: Profile) -> AuthProvider:
    """Return a `PatAuth` or `BasicAuth` provider for the profile.

    Raises `NotImplementedError` on `profile.auth == "oauth2"` pointing at
    `dhis2w_core` — OAuth2 needs the token-store machinery that lives there.
    Raises `ValueError` when the matching credential fields are missing.
    """
    if profile.auth == "pat":
        if not profile.token:
            raise ValueError("profile.auth == 'pat' but no token is set")
        return PatAuth(token=profile.token)
    if profile.auth == "basic":
        if not (profile.username and profile.password):
            raise ValueError("profile.auth == 'basic' but username/password are missing")
        return BasicAuth(username=profile.username, password=profile.password)
    if profile.auth == "oauth2":
        raise NotImplementedError(
            "OAuth2 auth needs the token store in dhis2w-core. "
            "Install it (`uv add dhis2w-core`) and call `dhis2w_core.open_client(profile)` instead."
        )
    raise ValueError(f"unknown profile.auth value: {profile.auth!r}")

open_client(profile, *, allow_version_fallback=True, retry_policy=None, http_limits=None, system_cache_ttl=300.0) async

Open a connected Dhis2Client for profile — PAT or Basic auth only.

Yields a connected client inside async with. Raises NotImplementedError on OAuth2 profiles — use dhis2w_core.open_client(profile, scope=..., profile_name=...) for those.

retry_policy, http_limits, and system_cache_ttl mirror the underlying Dhis2Client constructor — see its docstring for tuning.

Source code in packages/dhis2w-client/src/dhis2w_client/v42/client_context.py
@asynccontextmanager
async def open_client(
    profile: Profile,
    *,
    allow_version_fallback: bool = True,
    retry_policy: RetryPolicy | None = None,
    http_limits: httpx.Limits | None = None,
    system_cache_ttl: float | None = 300.0,
) -> AsyncGenerator[Dhis2Client]:
    """Open a connected `Dhis2Client` for `profile` — PAT or Basic auth only.

    Yields a connected client inside `async with`. Raises `NotImplementedError`
    on OAuth2 profiles — use `dhis2w_core.open_client(profile, scope=..., profile_name=...)`
    for those.

    `retry_policy`, `http_limits`, and `system_cache_ttl` mirror the
    underlying `Dhis2Client` constructor — see its docstring for tuning.
    """
    auth = build_auth_for_basic(profile)
    async with Dhis2Client(
        profile.base_url,
        auth=auth,
        version=None,
        allow_version_fallback=allow_version_fallback,
        retry_policy=retry_policy,
        http_limits=http_limits,
        system_cache_ttl=system_cache_ttl,
    ) as client:
        yield client