Source code for arize.users.client

"""Client implementation for managing users in the Arize platform."""

from __future__ import annotations

import logging
from typing import TYPE_CHECKING

from arize.pre_releases import ReleaseStage, prerelease_endpoint
from arize.users.types import (
    BulkUserDeletionResult,
    DeletionStatus,
    User,
    UserListResponse,
)
from arize.utils.resolve import NotFoundError, _find_user_id_by_email

if TYPE_CHECKING:
    import builtins

    from arize._generated.api_client.api_client import ApiClient
    from arize.config import SDKConfiguration
    from arize.users.types import (
        CustomUserRole,
        InviteMode,
        PredefinedUserRole,
        UserStatus,
    )

logger = logging.getLogger(__name__)


[docs] class UsersClient: """Client for managing Arize users. Unlike organizations, users are looked up by ID only — not by name — because display names are not unique within an account. This class is primarily intended for internal use within the SDK. Users are highly encouraged to access resource-specific functionality via :class:`arize.ArizeClient`. The users client is a thin wrapper around the generated REST API client, using the shared generated API client owned by :class:`arize.config.SDKConfiguration`. """ def __init__( self, *, sdk_config: SDKConfiguration, generated_client: ApiClient ) -> None: """ Args: sdk_config: Resolved SDK configuration. generated_client: Shared generated API client instance. """ # noqa: D205, D212 self._sdk_config = sdk_config # Import at runtime so it's still lazy and extras-gated by the parent from arize._generated import api_client as gen # Use the provided client directly self._api = gen.UsersApi(generated_client)
[docs] @prerelease_endpoint(key="users.list", stage=ReleaseStage.ALPHA) def list( self, *, email: str | None = None, status: list[UserStatus] | None = None, limit: int = 50, cursor: str | None = None, ) -> UserListResponse: """List users in the account. This endpoint supports cursor-based pagination. When provided, ``email`` filters results to users whose email contains the given substring (case-insensitive). Args: email: Optional case-insensitive partial-match filter on email address. status: Optional list of statuses to filter by (e.g. ``["active", "invited"]``). limit: Maximum number of users to return (1-100). The server enforces an upper bound of 100. cursor: Opaque pagination cursor from a previous response. Returns: A paginated user list response from the Arize REST API. Raises: ApiException: If the API request fails. """ return UserListResponse.model_validate( self._api.users_list( limit=limit, cursor=cursor, email=email, status=status, ), from_attributes=True, )
[docs] @prerelease_endpoint(key="users.get", stage=ReleaseStage.ALPHA) def get(self, *, user: str) -> User | None: """Get a user by ID or email address. When ``user`` contains ``@`` it is treated as an email address: :meth:`list` is called with the email as a filter, and the first result whose address matches exactly (case-insensitive) is returned. ``None`` is returned when no such user exists. When ``user`` does not contain ``@`` it is treated as a user ID and the API is called directly. An :class:`ApiException` is raised if the user is not found. Args: user: User ID or exact email address to retrieve. Returns: The matching :class:`~arize.users.types.User`, or ``None`` when ``user`` is an email address and no match exists. Raises: ApiException: If the API request fails (for example, user not found when querying by ID). """ if "@" in user: response = self.list(email=user) needle = user.lower() for u in response.users: if u.email.lower() == needle: return u return None return User.model_validate( self._api.users_get(user_id=user), from_attributes=True )
[docs] @prerelease_endpoint(key="users.create", stage=ReleaseStage.ALPHA) def create( self, *, name: str, email: str, role: PredefinedUserRole | CustomUserRole, invite_mode: InviteMode, is_developer: bool | None = None, ) -> User: """Create a new user. Args: name: Display name for the user (1-255 characters). email: Email address (used as the idempotency key). role: Account-level role assignment. Use ``PredefinedUserRole(name="<role>")`` for predefined roles (``admin``, ``member``, ``annotator``), or ``CustomUserRole(id="<role-id>")`` for custom RBAC roles. invite_mode: Invite mode (``"none"``, ``"email_link"``, or ``"temporary_password"``). is_developer: Whether the user should have developer permissions (can create GraphQL API keys). Defaults to ``True`` for ``admin`` and ``member`` roles, and ``False`` for ``annotator``. Returns: The created user object. Raises: ApiException: If the API request fails. """ from arize._generated import api_client as gen kwargs = {} if is_developer is not None: kwargs["is_developer"] = is_developer body = gen.CreateUserRequest( name=name, email=email, role=gen.UserRoleAssignment( gen.PredefinedUserRoleAssignment( type=gen.UserRoleAssignmentType.PREDEFINED, name=role.name, ) if role.type == "predefined" else gen.CustomUserRoleAssignment( type=gen.UserRoleAssignmentType.CUSTOM, id=role.id, ) ), invite_mode=invite_mode, **kwargs, ) return User.model_validate( self._api.users_create(create_user_request=body), from_attributes=True, )
[docs] @prerelease_endpoint(key="users.update", stage=ReleaseStage.ALPHA) def update( self, *, user_id: str, name: str | None = None, is_developer: bool | None = None, ) -> User: """Update a user's metadata by ID. Args: user_id: User ID to update. name: Updated display name for the user. is_developer: Updated developer permission flag. Returns: The updated user object. Raises: ValueError: If neither ``name`` nor ``is_developer`` is provided. ApiException: If the API request fails (for example, user not found or insufficient permissions). """ if name is None and is_developer is None: raise ValueError( "At least one of 'name' or 'is_developer' must be provided" ) from arize._generated import api_client as gen body = gen.UserUpdate( name=name, is_developer=is_developer, ) return User.model_validate( self._api.users_update(user_id=user_id, user_update=body), from_attributes=True, )
[docs] @prerelease_endpoint(key="users.delete", stage=ReleaseStage.ALPHA) def delete(self, *, user_id: str) -> None: """Delete a user by ID. This operation soft-deletes the user and cascades to organization memberships, space memberships, API keys, and role bindings. Args: user_id: User ID to delete. Returns: This method returns None on success (204 No Content response). Raises: ApiException: If the API request fails (for example, user not found or insufficient permissions). """ return self._api.users_delete(user_id=user_id)
[docs] @prerelease_endpoint( key="users.resend_invitation", stage=ReleaseStage.ALPHA ) def resend_invitation(self, *, user_id: str) -> None: """Resend an invitation email for a pending user. The target user must be in the ``invited`` state. Args: user_id: User ID to resend the invitation for. Returns: This method returns None on success (202 Accepted response). Raises: ApiException: If the API request fails (for example, user not found or user already active). """ return self._api.users_resend_invitation(user_id=user_id)
[docs] @prerelease_endpoint(key="users.bulk_delete", stage=ReleaseStage.ALPHA) def bulk_delete( self, *, user_ids: builtins.list[str] | None = None, emails: builtins.list[str] | None = None, ) -> builtins.list[BulkUserDeletionResult]: """Bulk-delete users by ID or email. At least one of ``user_ids`` or ``emails`` must be provided. When ``emails`` is given, each address is resolved to a user ID via the users list endpoint (case-insensitive exact match). Args: user_ids: User IDs to delete directly. emails: Email addresses to resolve and then delete. Returns: A list of :class:`~arize.users.types.BulkUserDeletionResult` recording the outcome of each deletion attempt. Raises: ValueError: If neither ``user_ids`` nor ``emails`` is provided. """ if not user_ids and not emails: raise ValueError( "At least one of 'user_ids' or 'emails' must be provided" ) results: builtins.list[BulkUserDeletionResult] = [] ids_to_delete: builtins.list[str] = user_ids or [] # Resolve emails to user IDs email: str for email in emails or []: try: ids_to_delete.append(_find_user_id_by_email(self._api, email)) except NotFoundError: # noqa: PERF203 logger.warning( "No user found with email '%s' — skipping", email, ) results.append( BulkUserDeletionResult( id=email, status=DeletionStatus.NOT_FOUND, error=f"No user found with email '{email}'", ) ) # Delete each user for uid in ids_to_delete: try: self.delete(user_id=uid) results.append( BulkUserDeletionResult( id=uid, status=DeletionStatus.DELETED ) ) except Exception as exc: # noqa: PERF203 results.append( BulkUserDeletionResult( id=uid, status=DeletionStatus.FAILED, error=str(exc), ) ) return results
[docs] @prerelease_endpoint(key="users.reset_password", stage=ReleaseStage.ALPHA) def reset_password(self, *, user_id: str) -> None: """Trigger a password-reset email for a user. Generates a reset token and sends the user a password-reset email with a 30-minute link. The target user must authenticate via password (not SSO/SAML) and must have already verified their account. Args: user_id: User ID to send the password-reset email to. Returns: This method returns None on success (204 No Content response). Raises: ApiException: If the API request fails (for example, user not found, user authenticates via SSO, or user has not yet verified their account). """ return self._api.users_password_reset(user_id=user_id)