# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/classes/50_DomoUser.ipynb.

# %% auto 0
__all__ = ['CreateUser_MissingRole', 'DownloadAvatar_NoAvatarKey', 'DomoUser', 'DomoUsers', 'DomoUser_NoSearch']

# %% ../../nbs/classes/50_DomoUser.ipynb 2
from domolibrary.routes.user import (
    UserProperty,
    UserProperty_Type,
    GetUser_Error,
    SearchUser_NoResults,
    User_CrudError,
    ResetPassword_PasswordUsed,
    DeleteUser_Error,
    DownloadAvatar_Error,
    UserProperty_Type
)


from ..routes.instance_config_sso import (SSO_AddUserDirectSignonError)
from ..utils.Image import Image, are_same_image, domo_default_img

# %% ../../nbs/classes/50_DomoUser.ipynb 3
import datetime as dt
from dataclasses import dataclass, field
from typing import Optional, List, Any, Union
import httpx

from nbdev.showdoc import patch_to
import asyncio

import domolibrary.utils.DictDot as util_dd
from domolibrary.utils.convert import (test_valid_email, convert_epoch_millisecond_to_datetime)

import domolibrary.client.DomoAuth as dmda
import domolibrary.client.Logger as lc
import domolibrary.client.DomoError as dmde
import domolibrary.routes.user as user_routes
import domolibrary.routes.instance_config_sso as sso_routes

# %% ../../nbs/classes/50_DomoUser.ipynb 7
class CreateUser_MissingRole(dmde.DomoError):
    def __init__(self, domo_instance, email_address):
        super().__init__(
            domo_instance=domo_instance,
            message=f"error creating user {email_address} missing role_id",
        )


class DownloadAvatar_NoAvatarKey(dmde.DomoError):
    def __init__(
        self,
        domo_instance,
        user_id,
    ):
        super().__init__(
            domo_instance,
            message=f"This profile {user_id} doesn't have an avatar uploaded - unable to download",
        )

# %% ../../nbs/classes/50_DomoUser.ipynb 9
@dataclass
class DomoUser:
    """a class for interacting with a Domo User"""

    auth: dmda.DomoAuth = field(repr=False)

    id: str
    display_name: str = None
    email_address: str = None
    role_id: str = None
    department: str = None
    title: str = None
    avatar_key: str = None
    password : str = field(repr = False, default = None)

    phone_number: str = None
    web_landing_page: str = None
    web_mobile_landing_page: str = None
    employee_id: str = None
    employee_number: str = None
    hire_date: str = None
    reports_to: str = None

    publisher_domain: str = None
    subscriber_domain: str = None
    virtual_user_id: str = None

    created_dt : dt.datetime = None
    last_activity_dt: dt.datetime = None

    custom_attributes: dict = field(default_factory=dict)

    role: Any = None  # DomoRole
    domo_api_clients : List[Any] = None
    domo_access_tokens : List[Any] = None


    def __post_init__(self):
        self.id = str(self.id)

    def __eq__(self, other):
        if self.__class__.__name__ != other.__class__.__name__:
            return False

        return self.id == other.id

    @classmethod
    def _from_search_json(cls, auth, user_obj):
        user_dd = util_dd.DictDot(user_obj)

        return cls(
            auth=auth,
            id=str(user_dd.id or user_dd.userId),
            display_name=user_dd.displayName,
            title=user_dd.title,
            department=user_dd.department,
            email_address=user_dd.emailAddress or user_dd.email,
            role_id=user_dd.roleId,
            avatar_key=user_dd.avatarKey,
            phone_number=user_dd.phoneNumber,
            web_landing_page=user_dd.webLandingPage,
            web_mobile_landing_page=user_dd.webMobileLandingPage,
            employee_id=user_dd.employeeId,
            employee_number=user_dd.employeeNumber,
            hire_date=user_dd.hireDate,
            reports_to=user_dd.reportsTo,
            created_dt = convert_epoch_millisecond_to_datetime(user_dd.created),
            last_activity_dt = convert_epoch_millisecond_to_datetime(user_dd.lastActivity)
        )

    @classmethod
    def _from_virtual_json(cls, auth, user_obj):
        user_dd = util_dd.DictDot(user_obj)

        return cls(
            id=user_dd.id,
            auth=auth,
            publisher_domain=user_dd.publisherDomain,
            subscriber_domain=user_dd.subscriberDomain,
            virtual_user_id=user_dd.virtualUserId,
        )

    @classmethod
    def _from_bootstrap_json(cls, auth, user_obj):
        dd = user_obj
        if isinstance(user_obj, dict):
            dd = util_dd.DictDot(user_obj)

        return cls(id=dd.id, display_name=dd.displayName, auth=auth)

# %% ../../nbs/classes/50_DomoUser.ipynb 11
@patch_to(DomoUser)
async def get_role(
    self: DomoUser,
    debug_api: bool = False,
    debug_num_stacks_to_drop=2,
    session: httpx.AsyncClient = None,
):
    import domolibrary.classes.DomoRole as dmr

    self.role = await dmr.DomoRole.get_by_id(
        role_id=self.role_id,
        auth=self.auth,
        debug_api=debug_api,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        session=session,
    )

    return self.role

# %% ../../nbs/classes/50_DomoUser.ipynb 12
@patch_to(DomoUser, cls_method=True)
async def get_by_id(
    cls: DomoUser,
    user_id,
    auth: dmda.DomoAuth,
    return_raw: bool = False,
    debug_api: bool = False,
    debug_num_stacks_to_drop=2,
    session: httpx.AsyncClient = None,
):
    """
    searches and returns a domo user
    will throw an error if no user returned with an option to suppress_no_results_error
    """

    res = await user_routes.get_by_id(
        auth=auth,
        user_id=user_id,
        debug_api=debug_api,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        session=session,
        parent_class=cls.__name__,
    )

    if return_raw:
        return res

    if not res.is_success:
        return None

    domo_user = cls._from_search_json(user_obj=res.response, auth=auth)
    
    try:
        await domo_user.get_role(debug_api = debug_api, debug_num_stacks_to_drop = debug_num_stacks_to_drop, session = session)
    
    except Exception as e:
        print(e)


    return domo_user

# %% ../../nbs/classes/50_DomoUser.ipynb 15
@patch_to(DomoUser)
async def download_avatar(
    self: DomoUser,
    pixels: int = 300,
    folder_path="./images",
    img_name=None,  # will default to user_id
    auth: dmda.DomoAuth = None,
    is_download_image: bool = True,  # option to prevent downloading the image file
    debug_api: bool = False,
    return_raw: bool = False,
):
    """downloads a user's avatar to a folder
    and returns the byte representation of the image
    """
    auth = auth or self.auth

    # if not self.avatar_key:
    #     raise DownloadAvatar_NoAvatarKey(
    #         domo_instance=auth.domo_instance, user_id=self.id
    #     )

    res = await user_routes.download_avatar(
        auth=self.auth,
        user_id=self.id,
        pixels=pixels,
        folder_path=folder_path,
        img_name=img_name,
        is_download_image=is_download_image,
        debug_api=debug_api,
    )

    if return_raw:
        return res

    self.avatar = Image.from_bytestr(data=res.response)

    return self.avatar

    # return res.response

# %% ../../nbs/classes/50_DomoUser.ipynb 19
@patch_to(DomoUser)
async def update_properties(
    self: DomoUser,
    property_ls: List[
        UserProperty
    ],  # use the UserProperty class to define a list of user properties to update, see user route documentation to see a list of UserProperty_Types that can be updated
    return_raw: bool = False,
    auth: dmda.DomoAuth = None,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
):
    auth = auth or self.auth

    res = await user_routes.update_user(
        auth=auth,
        user_id=self.id,
        user_property_ls=property_ls,
        debug_api=debug_api,
        session=session,
    )
    if return_raw:
        return res

    self = await DomoUser.get_by_id(user_id=self.id, auth=auth)

    return self


@patch_to(DomoUser)
async def set_user_landing_page(
    self: DomoUser,
    page_id: str,
    user_id: str = None,
    auth: dmda.DomoAuth = None,
    debug_api: bool = False,
):
    res = await user_routes.set_user_landing_page(
        auth=auth or self.auth,
        page_id=page_id,
        user_id=self.id or user_id,
        debug_api=debug_api,
    )

    return res

# %% ../../nbs/classes/50_DomoUser.ipynb 22
@patch_to(DomoUser, cls_method= True)
async def create(
    cls,
    auth : dmda.DomoAuth,
    display_name,
    email_address,
    role_id,
    password: str = None,
    send_password_reset_email: bool = False,
    debug_api: bool = False,
    debug_num_stacks_to_drop: int = 2,
    session: httpx.AsyncClient = None,
):
    """class method that creates a new Domo user"""

    res = await user_routes.create_user(
        auth=auth,
        display_name=display_name,
        email_address=email_address,
        role_id=role_id,
        debug_api=debug_api,
        session=session,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
    )

    domo_user = await DomoUser.get_by_id(
        auth=auth,
        user_id=res.response.get("id") or res.response.get("userId"),
    )

    if password:
        await domo_user.reset_password(new_password=password)

    elif send_password_reset_email:
        await domo_user.request_password_reset(
            domo_instance=auth.domo_instance, email=domo_user.email_address
        )

    return domo_user



@patch_to(DomoUser)
async def delete(
    self: DomoUser,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    debug_num_stacks_to_drop=2,
    parent_class=None,
):

    res = await user_routes.delete_user(
        auth=self.auth,
        user_id=self.id,
        debug_api=debug_api,
        session=session,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        parent_class=parent_class,
    )

    return res

# %% ../../nbs/classes/50_DomoUser.ipynb 26
@patch_to(DomoUser)
async def reset_password(
    self: DomoUser,
    new_password: str,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    debug_num_stacks_to_drop: int = 2,
):
    """reset your password, will respect password restrictions set up in the Domo UI"""

    res = await user_routes.reset_password(
        auth=self.auth,
        user_id=self.id,
        new_password=new_password,
        debug_api=debug_api,
        session=session,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        parent_class = self.__class__.__name__
    )

    self.password = new_password

    return res


@patch_to(DomoUser, cls_method=True)
async def request_password_reset(
    cls,
    domo_instance: str,
    email: str,
    locale: str = "en-us",
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    debug_num_stacks_to_drop : int = 2
):
    """request password reset email.  Note: does not require authentication."""

    return await user_routes.request_password_reset(
        domo_instance=domo_instance,
        email=email,
        locale=locale,
        debug_api=debug_api,
        session=session,
        debug_num_stacks_to_drop = debug_num_stacks_to_drop,
        parent_class = cls.__name__
    )

# %% ../../nbs/classes/50_DomoUser.ipynb 27
@patch_to(DomoUser)
async def upload_avatar(
    self,
    avatar: Image,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    return_raw: bool = False,
):
    avatar.crop_square()

    res = await user_routes.upload_avatar(
        auth=self.auth,
        user_id=self.id,
        img_bytestr=avatar.to_bytes(),
        img_type=avatar.format,
        debug_api=debug_api,
        parent_class=self.__class__.__name__,
        session=session,
    )

    if return_raw:
        return res

    await asyncio.sleep(2)

    return await self.download_avatar(debug_api=debug_api)

# %% ../../nbs/classes/50_DomoUser.ipynb 29
@patch_to(DomoUser)
async def upsert_avatar(
    self,
    avatar: Image,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    return_raw: bool = False,
):
    avatar.crop_square()

    res = "images are the same"
    if not are_same_image(domo_default_img, avatar):

        res = await user_routes.upload_avatar(
            auth=self.auth,
            user_id=self.id,
            img_bytestr=avatar.to_bytes(),
            img_type=avatar.format,
            debug_api=debug_api,
            parent_class=self.__class__.__name__,
            session=session,
        )

        if return_raw:
            return res

    await asyncio.sleep(2)

    return await self.download_avatar(debug_api=debug_api)

# %% ../../nbs/classes/50_DomoUser.ipynb 31
@patch_to(DomoUser)
async def toggle_direct_signon_access(self : DomoUser,
                                      is_enable_direct_signon : bool = True,
                                      session: httpx.AsyncClient = None,
                                      debug_api : bool =False,
                                      debug_num_stacks_to_drop : int= 2 ):
    
    res = await sso_routes.toggle_user_direct_signon_access(auth= self.auth,
        user_id_ls = [self.id],
        is_enable_direct_signon =is_enable_direct_signon,
        session = session,
        debug_api = debug_api,
        parent_class= self.__class__.__name__,
        debug_num_stacks_to_drop= debug_num_stacks_to_drop)
    
    return res

# %% ../../nbs/classes/50_DomoUser.ipynb 32
@patch_to(DomoUser)
async def get_api_clients(
    self,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
    debug_num_stacks_to_drop=2,
    return_raw: bool = False,
):
    """
    retrieves Client_IDs for this user (assuming the authenticated user has manage rights).
    Note : the values will be masked, raw text values can only be retrieved via the UI
    """

    import domolibrary.classes.DomoInstanceConfig_ApiClient as dicli

    api_clients = dicli.ApiClients(auth=auth)
    domo_clients = await api_clients.get(
        session=session,
        debug_api=debug_api,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop+1,
    )

    if return_raw: return res

    domo_clients = [
        domo_client
        for domo_client in domo_clients
        if domo_client.domo_user.id == self.id
    ]

    if not domo_clients:
        print(f"Domo User {self.id} - {self.display_name} does not have any Client_IDs")
        return False

    self.domo_api_clients = domo_clients

    return self.domo_api_clients

# %% ../../nbs/classes/50_DomoUser.ipynb 33
@patch_to(DomoUser)
async def get_access_tokens(
    self,
    debug_api: bool = False,
    debug_num_stacks_to_drop : int = 2,
    session: httpx.AsyncClient = None,
    return_raw: bool = False,
):
    import domolibrary.classes.DomoAccessToken as dmat

    domo_config = dmat.DomoAccessTokens(auth = self.auth)
    domo_tokens = await domo_config.get(
        debug_api=debug_api,
        session=session,
        debug_num_stacks_to_drop= debug_num_stacks_to_drop + 1
    )

    if return_raw:
        return domo_tokens
    
    domo_tokens = [domo_token for domo_token in domo_tokens if domo_token.owner.id == self.id]

    if not domo_tokens:
        print(f"Domo User {self.id} - {self.display_name} does not have any access tokens")
        return []
     
    self.domo_access_tokens = domo_tokens

    return self.domo_access_tokens

# %% ../../nbs/classes/50_DomoUser.ipynb 36
@dataclass
class DomoUsers:
    """a class for searching for Users"""

    auth: dmda.DomoAuth = field(repr=False)

    users: List[DomoUser] = None
    logger: Optional[lc.Logger] = None

    @classmethod
    def _users_to_domo_user(cls, user_ls, auth: dmda.DomoAuth):
        return [
            DomoUser._from_search_json(auth=auth, user_obj=user_obj)
            for user_obj in user_ls
        ]

    @classmethod
    def _users_to_virtual_user(cls, user_ls, auth: dmda.DomoAuth):
        return [
            DomoUser._from_virtual_json(auth=auth, user_obj=user_obj)
            for user_obj in user_ls
        ]

    def _generate_logger(self, logger: Optional[lc.Logger] = None):
        self.logger = logger or self.logger or lc.Logger(app_name="domo_users")

    @staticmethod
    def _util_match_domo_users_to_emails(
        domo_users: list[DomoUser], user_email_ls: list[str]
    ) -> list:
        """pass in an array of user emails to match against an array of Domo User"""

        return [
            domo_user
            for domo_user in domo_users
            if domo_user.email_address.lower() in [email.lower() for email in user_email_ls]
        ]

    @staticmethod
    def _util_match_users_obj_to_emails(
        user_ls: list[dict], user_email_ls: list[str]
    ) -> list:
        """pass in an array of user emails to match against an array of Domo User"""

        return [
            obj
            for obj in user_ls
            if obj.get("emailAddress").lower() in [email.lower() for email in user_email_ls]
        ]


# %% ../../nbs/classes/50_DomoUser.ipynb 38
@patch_to(DomoUsers)
async def get(
    self,
    return_raw: bool = False,
    debug_api: bool = False,
    debug_num_stacks_to_drop=2,
    session : httpx.AsyncClient = None,
) -> List[DomoUser]:
    """retrieves all users from Domo"""

    res = await user_routes.get_all_users(
        auth=self.auth,
        debug_api=debug_api,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        parent_class=self.__class__.__name__,
        session = session
    )

    if return_raw:
        return res

    self.users = self._users_to_domo_user(user_ls=res.response, auth=self.auth)
    return self.users

@patch_to(DomoUsers, cls_method=True)
async def all_users(
    cls: DomoUsers,
    auth: dmda.DomoAuth,
    return_raw: bool = False,
    debug_api: bool = False,
    debug_num_stacks_to_drop=2,
    session : httpx.AsyncClient = None,
    logger: Optional[lc.Logger] = None,
) -> List[DomoUser]:
    """retrieves all users from Domo"""

    res = await user_routes.get_all_users(
        auth=auth,
        debug_api=debug_api,
        debug_num_stacks_to_drop=debug_num_stacks_to_drop,
        parent_class=cls.__name__,
        session = session
    )

    if return_raw:
        return res

    if not res.is_success:
        return None

    users_ls = res.response

    return cls._users_to_domo_user(user_ls=users_ls, auth=auth)

# %% ../../nbs/classes/50_DomoUser.ipynb 40
class DomoUser_NoSearch(dmde.ClassError):
    def __init__(self, message : str, domo_instance , cls_instance = None, cls = None):
        super().__init__(cls= cls, cls_instance=cls_instance, message = message,
                         entity_id = domo_instance
                         )

# %% ../../nbs/classes/50_DomoUser.ipynb 41
@patch_to(DomoUsers)
async def search_by_email(
    self,
    email: Union[str, list],
    only_allow_one: bool = True,
    debug_api: bool = False,
    debug_num_stacks_to_drop=2,
    return_raw: bool = False,
    suppress_no_results_error: bool = False,
    session: httpx.AsyncClient = None,
) -> list:

    emails = [email] if isinstance(email, str) else email

    try:
        res = await user_routes.search_users_by_email(
            user_email_ls=emails,
            auth=self.auth,
            return_raw=return_raw,
            debug_api=debug_api,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
            parent_class=self.__class__.__name__,
            session=session,
        )

    except SearchUser_NoResults as e:
        if suppress_no_results_error:
            return None

        raise e from e

    if return_raw:
        return res

    domo_users = self._users_to_domo_user(res.response, auth=self.auth)

    if not only_allow_one:
        return domo_users

    domo_users = self._util_match_domo_users_to_emails(domo_users, emails)

    if not domo_users:
        raise DomoUser_NoSearch(
            cls_instance=self, message=f'unable to find {",".join(emails)}',
            domo_instance = self.auth.domo_instance
        )

    return domo_users[0]


@patch_to(DomoUsers, cls_method=True)
async def by_email(
    cls: DomoUsers,
    email_ls: list[str],
    auth: dmda.DomoAuth,
    only_allow_one: bool = True,
    debug_api: bool = False,
    debug_num_stacks_to_drop=2,
    return_raw: bool = False,
    suppress_no_results_error: bool = False,
    session: httpx.AsyncClient = None,
) -> list:

    try:
        res = await user_routes.search_users_by_email(
            user_email_ls=email_ls,
            auth=auth,
            return_raw=False,
            debug_api=debug_api,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
            parent_class=cls.__name__,
            session=session,
        )
    except SearchUser_NoResults as e:
        if suppress_no_results_error:
            return None

        raise e

    if return_raw:
        return res

    domo_users = cls._users_to_domo_user(res.response, auth=auth)

    if not only_allow_one:
        return domo_users

    domo_users = cls._util_match_domo_users_to_emails(domo_users, email_ls)

    if not domo_users:
        raise DomoUser_NoSearch(cls=cls, message=f'unable to find {",".join(email_ls)}', domo_instance = auth.domo_instance)

    return domo_users[0]

@patch_to(DomoUsers, cls_method=True)
async def by_id(
    cls: DomoUsers,
    auth: dmda.DomoAuth,
    user_ids: list[str],  # can search for one or multiple users
    suppress_no_results_error: bool = False,
    only_allow_one: bool = True,
    debug_num_stacks_to_drop=2,
    debug_api: bool = False,
    return_raw: bool = False,
    session : httpx.AsyncClient = None
) -> list:
    
    res = None

    try:
        res = await user_routes.search_users_by_id(
            return_raw=False,
            user_ids=user_ids,
            debug_api=debug_api,
            auth=auth,
            debug_num_stacks_to_drop=debug_num_stacks_to_drop,
            parent_class=cls.__name__,
            session = session
        )

    except SearchUser_NoResults as e:
        if suppress_no_results_error:
            return None

    if return_raw:
        return res

    domo_users = cls._users_to_domo_user(user_ls=res.response, auth=auth)

    if only_allow_one:
        return domo_users[0]

    return domo_users

# %% ../../nbs/classes/50_DomoUser.ipynb 44
@patch_to(DomoUsers, cls_method=True)
async def virtual_user_by_subscriber_instance(
    cls: DomoUsers,
    subscriber_instance_ls: str,
    auth: dmda.DomoAuth,
    debug_api: bool = False,
    return_raw: bool = False,
):
    res = await user_routes.search_virtual_user_by_subscriber_instance(
        auth=auth,
        subscriber_instance_ls=subscriber_instance_ls,
        debug_api=debug_api,
    )

    if return_raw:
        return res


    domo_users = cls._users_to_virtual_user(res.response, auth=auth)
    return domo_users

# %% ../../nbs/classes/50_DomoUser.ipynb 47
@patch_to(DomoUsers, cls_method=True)
async def create_user(
    cls: DomoUsers,
    auth: dmda.DomoAuth,
    display_name,
    email_address,
    role_id,
    password: str = None,
    send_password_reset_email: bool = False,
    debug_api: bool = False,
    session: httpx.AsyncClient = None,
):
    """class method that creates a new Domo user"""

    res = await user_routes.create_user(
        auth=auth,
        display_name=display_name,
        email_address=email_address,
        role_id=role_id,
        debug_api=debug_api,
        session=session,
    )

    domo_user = await DomoUser.get_by_id(
        auth=auth,
        user_id=res.response.get("id") or res.response.get("userId"),
    )

    if password:
        await domo_user.reset_password(new_password=password)

    elif send_password_reset_email:
        await domo_user.request_password_reset(
            domo_instance=auth.domo_instance, email=domo_user.email_address
        )

    return domo_user

# %% ../../nbs/classes/50_DomoUser.ipynb 49
@patch_to(DomoUsers)
async def upsert(
    self: DomoUsers,
    email_address: str,
    display_name: str = None,
    role_id: str = None,
    debug_api: bool = False,
    debug_num_stacks_to_drop: int = 2,
    session: httpx.AsyncClient = None,
):
    test_valid_email(email_address)

    try:
        domo_user = await self.search_by_email(
            email = email_address,
            only_allow_one=True,
            debug_api=debug_api,
            session = session, 
            debug_num_stacks_to_drop = debug_num_stacks_to_drop + 1
        )

        if domo_user:
            user_property_ls = []
            if display_name:
                user_property_ls.append(
                    user_routes.UserProperty(
                        user_routes.UserProperty_Type.display_name, display_name
                    )
                )

            if role_id:
                user_property_ls.append(
                    user_routes.UserProperty(
                        user_routes.UserProperty_Type.role_id, role_id
                    )
                )

            if user_property_ls:
                await user_routes.update_user(
                    user_id=domo_user.id,
                    user_property_ls=user_property_ls,
                    auth=self.auth,
                    debug_api=debug_api,
                )
        return await DomoUser.get_by_id(auth=self.auth, user_id=domo_user.id)

    except SearchUser_NoResults as e:

        if not role_id:
            raise CreateUser_MissingRole(
                domo_instance=self.auth.domo_instance, email_address=email_address
            ) from e

        domo_user = await DomoUser.create(
            display_name=display_name or f"{email_address} - via dl {dt.date.today()}",
            email_address=email_address,
            role_id=role_id,
            debug_api=debug_api,
            session=session,
            auth = self.auth
        )

        await self.get()

        return domo_user

    # finally:
    #     if grant_ls:
    #         grant_ls = domo_role._valid_grant_ls(grant_ls)
    #         await domo_role.set_grants(grant_ls=grant_ls)

@patch_to(DomoUsers, cls_method=True)
async def upsert_user(
    cls: DomoUsers,
    auth: dmda.DomoAuth,
    email_address: str,
    display_name: str = None,
    role_id: str = None,
    debug_api: bool = False,
    debug_prn: bool = False,
    session: httpx.AsyncClient = None,
):
    test_valid_email(email_address)

    try:
        domo_user = await cls.by_email(
            email_ls=[email_address],
            auth=auth,
            only_allow_one=True,
            debug_api=debug_api,
        )

        if domo_user:
            user_property_ls = []
            if display_name:
                user_property_ls.append(
                    user_routes.UserProperty(
                        user_routes.UserProperty_Type.display_name, display_name
                    )
                )

            if role_id:
                user_property_ls.append(
                    user_routes.UserProperty(
                        user_routes.UserProperty_Type.role_id, role_id
                    )
                )

            if user_property_ls:
                await user_routes.update_user(
                    user_id=domo_user.id,
                    user_property_ls=user_property_ls,
                    auth=auth,
                    debug_api=debug_api,
                )
        return await DomoUser.get_by_id(auth=auth, user_id=domo_user.id)

    except SearchUser_NoResults as e:
        if debug_prn:
            print(f"No user match -- creating new user in {auth.domo_instance}")

        if not role_id:
            raise CreateUser_MissingRole(
                domo_instance=auth.domo_instance, email_address=email_address
            ) from e

        return await cls.create_user(
            auth=auth,
            display_name=display_name or f"{email_address} - via dl {dt.date.today()}",
            email_address=email_address,
            role_id=role_id,
            debug_api=debug_api,
            session=session,
        )

    # finally:
    #     if grant_ls:
    #         grant_ls = domo_role._valid_grant_ls(grant_ls)
    #         await domo_role.set_grants(grant_ls=grant_ls)
