# AUTOGENERATED! DO NOT EDIT! File to edit: ../../nbs/client/95_DomoAuth.ipynb.

# %% auto 0
__all__ = ['get_full_auth', 'get_developer_auth', 'test_access_token', 'DomoAuth', 'InvalidCredentialsError',
           'InvalidAuthTypeError', 'InvalidInstanceError', 'NoAccessTokenReturned', 'DomoFullAuth', 'test_is_full_auth',
           'DomoTokenAuth', 'DomoDeveloperAuth', 'DomoJupyterAuth', 'DomoJupyterFullAuth', 'DomoJupyterTokenAuth',
           'test_is_jupyter_auth']

# %% ../../nbs/client/95_DomoAuth.ipynb 3
from dataclasses import dataclass, field
from typing import Optional, Union
from urllib.parse import urlparse


import httpx

import domolibrary.client.ResponseGetData as rgd
import domolibrary.client.Logger as lg
import domolibrary.client.DomoError as de

# %% ../../nbs/client/95_DomoAuth.ipynb 5
async def get_full_auth(
    domo_instance: str,  # domo_instance.domo.com
    domo_username: str,  # email address
    domo_password: str,
    session: Optional[httpx.AsyncClient] = None,
    debug_api: bool = False
) -> rgd.ResponseGetData:
    """uses username and password authentication to retrieve a full_auth access token"""

    is_close_session = False

    if not session:
        is_close_session = True
        session = httpx.AsyncClient()

    url = f"https://{domo_instance}.domo.com/api/content/v2/authentication"

    tokenHeaders = {"Content-Type": "application/json"}

    body = {
        "method": "password",
        "emailAddress": domo_username,
        "password": domo_password,
    }

    if debug_api:
        print(body, url)

    res = await session.request(method="POST", url=url, headers=tokenHeaders, json=body)

    if is_close_session:
        await session.aclose()

    return rgd.ResponseGetData._from_httpx_response(res)

# %% ../../nbs/client/95_DomoAuth.ipynb 12
async def get_developer_auth(
    domo_client_id: str,
    domo_client_secret: str,
    session: Optional[httpx.AsyncClient] = None,
    debug_api: bool = False
) -> rgd.ResponseGetData:

    """
    only use for authenticating against apis documented under developer.domo.com
    """
    is_close_session = False

    if not session:
        is_close_session = True
        session = httpx.AsyncClient(
            auth=httpx.BasicAuth(domo_client_id, domo_client_secret)
        )

    url = "https://api.domo.com/oauth/token?grant_type=client_credentials"

    if debug_api:
        print(url, domo_client_id, domo_client_secret)

    res = await session.request(method="GET", url=url)

    if is_close_session:
        await session.aclose()

    return rgd.ResponseGetData._from_httpx_response(res)

# %% ../../nbs/client/95_DomoAuth.ipynb 16
async def test_access_token(
    domo_access_token: str,  # as provided in Domo > Admin > Authentication > AccessTokens
    domo_instance: str,  # <domo_instance>.domo.com
    session: Optional[httpx.AsyncClient] = None,
    debug_api: bool = False
):
    """
    will attempt to validate against the 'me' API.
    This is the same authentication test the Domo Java CLI uses.
    """

    is_close_session = False

    if not session:
        is_close_session = True
        session = httpx.AsyncClient()

    url = f"https://{domo_instance}.domo.com/api/content/v2/users/me"

    tokenHeaders = {"X-DOMO-Developer-Token": domo_access_token}

    if debug_api:
        print(url,tokenHeaders)

    res = await session.request(method="GET", headers=tokenHeaders, url=url)

    if is_close_session:
        await session.aclose()

    return rgd.ResponseGetData._from_httpx_response(res)

# %% ../../nbs/client/95_DomoAuth.ipynb 21
@dataclass
class _DomoAuth_Required:
    """required parameters for all Domo Auth classes"""

    domo_instance: str

    def __post_init__(self):
        if self.domo_instance:
            self.set_manual_login()

    def set_manual_login(self):
        self.url_manual_login = (
            f"https://{self.domo_instance}.domo.com/auth/index?domoManualLogin=true"
        )


@dataclass
class _DomoAuth_Optional:
    """parameters are defined after initialization"""

    token: Optional[str] = field(default=None, repr=False)
    token_name: Optional[str] = field(default=None)
    user_id: Optional[str] = field(default=None, repr=False)
    auth_header: dict = field(default_factory=dict, repr=False)

    is_valid_token: bool = None

    url_manual_login: Optional[str] = None

    async def get_auth_token(self) -> Union[str, None]:
        """placeholder method"""
        pass

    async def generate_auth_header(self) -> Union[dict, None]:
        """returns auth header appropriate for this authentication method"""
        pass

    async def print_is_token(self, token_name=None) -> None:
        self.token_name = token_name or self.token_name

        if not self.token:
            await self.get_auth_token()

        token_str = f"{self.token_name} "

        if not self.token:
            print(
                f"🚧 failed to retrieve {token_str if token_name else ''}token from {self.domo_instance}")
            return False

        print(
            f"🎉 {token_str if token_name else ''}token retrieved from {self.domo_instance} ⚙️")
        return True


# %% ../../nbs/client/95_DomoAuth.ipynb 22
@dataclass
class DomoAuth(_DomoAuth_Optional, _DomoAuth_Required):
    """abstract DomoAuth class"""

# %% ../../nbs/client/95_DomoAuth.ipynb 26
class InvalidCredentialsError(de.DomoError):
    """return invalid credentials sent to API"""

    def __init__(
        self,
        function_name: Optional[str] = None,
        status: Optional[int] = None,  # API request status
        message="invalid credentials",
        domo_instance: Optional[str] = None,
    ):

        super().__init__(status=status, message=message, domo_instance=domo_instance)


class InvalidAuthTypeError(de.DomoError):
    """return invalid Auth type sent to API"""

    def __init__(
        self,
        required_auth_type :DomoAuth =None,
        required_auth_type_ls :DomoAuth =None,

        function_name: Optional[str] = None,
        domo_instance: Optional[str] = None,
    ):
        message = f"This API rquires {required_auth_type.__name__ if required_auth_type else ', '.join([auth_type.__name__ for auth_type in required_auth_type_ls])}"

        super().__init__(message=message, domo_instance=domo_instance, function_name = function_name)

class InvalidInstanceError(de.DomoError):
    """return if invalid domo_instance sent to API"""

    def __init__(
        self,
        function_name: Optional[str] = None,
        status: Optional[int] = None,
        message="invalid instance",
        domo_instance: Optional[str] = None,
    ):
        super().__init__(status=status, message=message, domo_instance=domo_instance)
    

class NoAccessTokenReturned(de.DomoError):
    def __init__(
        self,
        function_name: Optional[str] = None,
        status: Optional[int] = None,
        message :str = "No AccessToken returned",
        domo_instance: Optional[str] = None,
    ):
        super().__init__(status = status, message = message, domo_instance = domo_instance)

# %% ../../nbs/client/95_DomoAuth.ipynb 29
@dataclass
class _DomoFullAuth_Required:
    """mix requied parameters for DomoFullAuth"""

    domo_username: str
    domo_password: str = field(repr=False)


# %% ../../nbs/client/95_DomoAuth.ipynb 30
@dataclass
class DomoFullAuth(_DomoAuth_Optional, _DomoFullAuth_Required, _DomoAuth_Required):
    """use for full authentication token"""

    def generate_auth_header(self, token: str) -> dict:
        self.auth_header = {"x-domo-authentication": token}
        return self.auth_header

    async def get_auth_token(
        self,
        session: Optional[httpx.AsyncClient] = None,
        return_raw: bool = False,
        debug_api : bool = False
    ) -> str:
        """returns `token` if valid credentials provided else raises Exception and returns None"""

        res = await get_full_auth(
            domo_instance=self.domo_instance,
            domo_username=self.domo_username,
            domo_password=self.domo_password,
            session=session,
            debug_api = debug_api
        )

        if return_raw:
            return res


        if res.is_success and (
            res.response.get("reason") == "INVALID_CREDENTIALS" 
            or res.response.get("reason") == "ACCOUNT_LOCKED" ) :
            self.is_valid_token = False
            raise InvalidCredentialsError(
                function_name = "get_auth_token",
                status=res.status,
                message=str(res.response.get("reason")),
                domo_instance=self.domo_instance,
            )
            return None

        if res.status == 403: # invalid instance
            self.is_valid_token = False
            raise InvalidInstanceError(
                function_name = "get_auth_token",
                status=res.status,
                message="INVALID INSTANCE",
                domo_instance=self.domo_instance,
            )
            return None
        
        if res.is_success and ( res.response == {} or res.response == '') : # no access token
            self.is_valid_token = False
            raise NoAccessTokenReturned(
                function_name="get_auth_token",
                status=res.status, 
                domo_instance=self.domo_instance)

            return None


        self.is_valid_token = True

        token = str(res.response.get("sessionToken"))
        self.token = token
        self.user_id = str(res.response.get("userId"))

        self.auth_header = self.generate_auth_header(token=token)

        if not self.token_name:
            self.token_name = "full_auth"

        return self.token

# %% ../../nbs/client/95_DomoAuth.ipynb 35
def test_is_full_auth(auth, function_name=None, num_stacks_to_drop = 1 # pass q for route pass 2 for class
):
    
    tb= lg.get_traceback(num_stacks_to_drop=num_stacks_to_drop)

    function_name = function_name or tb.function_name


    if auth.__class__.__name__ != 'DomoFullAuth':
        raise InvalidAuthTypeError(function_name=function_name,
                                        domo_instance=auth.domo_instance,
                                        required_auth_type=DomoFullAuth)


# %% ../../nbs/client/95_DomoAuth.ipynb 37
@dataclass
class _DomoTokenAuth_Required:
    """mix requied parameters for DomoFullAuth"""

    domo_access_token: str = field(repr=False)

# %% ../../nbs/client/95_DomoAuth.ipynb 38
@dataclass
class DomoTokenAuth(_DomoAuth_Optional, _DomoTokenAuth_Required, _DomoAuth_Required):
    

    """
    use for access_token authentication.
    Tokens are generated in domo > admin > access token
    Necessary in cases where direct sign on is not permitted
    """

    def generate_auth_header(self, token: str) -> dict:
        self.auth_header = {"x-domo-developer-token": token}
        return self.auth_header

    async def get_auth_token(
        self, session: Optional[httpx.AsyncClient] = None,
        debug_api : bool = False
    ) -> str:
        """
        updates internal attributes
        having an access_token assumes pre-authenticaiton
        """

        res = await test_access_token(
            domo_instance=self.domo_instance,
            domo_access_token=self.domo_access_token,
            session=session,
            debug_api = debug_api
        )

        if res.status == 401 and res.response == "Unauthorized":
            self.is_valid_token = False
            raise InvalidCredentialsError(
                status=res.status,
                message=res.response,
                domo_instance=self.domo_instance,
            )

        if res.status == 200:
            self.is_valid_token = True

        self.token = self.domo_access_token
        self.user_id = res.response.get("id")

        self.auth_header = self.generate_auth_header(token=self.token)

        if not self.token_name:
            self.token_name = "token_auth"

        return self.token

# %% ../../nbs/client/95_DomoAuth.ipynb 42
@dataclass
class _DomoDeveloperAuth_Required:
    """mix requied parameters for DomoDeveloperAuth"""

    domo_client_id: str
    domo_client_secret: str = field(repr=False)

@dataclass
class _DomoDeveloperAuth_Optional:
    """mix optional parameters for DomoDeveloperAuth"""

    domo_instance: str = None  # because api.domo.com apis don't require domo_instance


# %% ../../nbs/client/95_DomoAuth.ipynb 43
@dataclass
# (init=False)
class DomoDeveloperAuth(_DomoDeveloperAuth_Optional, _DomoAuth_Optional, _DomoDeveloperAuth_Required):
    """use for full authentication token"""

    # def __init__(self, domo_client_id: str, domo_client_secret: str):
    #     self.domo_client_id = domo_client_id
    #     self.domo_client_secret = domo_client_secret
    #     self.domo_instance = ""

    def generate_auth_header(self, token: str) -> dict:
        self.auth_header = {"Authorization": "bearer " + token}
        return self.auth_header

    async def get_auth_token(
        self,
        session: Optional[httpx.AsyncClient] = None,
        debug_api : bool = False
    ) -> str:

        res = await get_developer_auth(
            domo_client_id=self.domo_client_id,
            domo_client_secret=self.domo_client_secret,
            session=session,
            debug_api = debug_api
        )

        if res.status == 401:
            self.is_valid_token = False
            raise InvalidCredentialsError(
                status=res.status,
                message=str(res.response),
                domo_instance=self.domo_instance,
            )

        self.is_valid_token = True

        token = str(res.response.get("access_token"))
        self.token = token
        self.user_id = res.response.get("userId")
        self.domo_instance = res.response.get("domain")
        self.set_manual_login()

        self.auth_header = self.generate_auth_header(token=token)

        if not self.token_name:
            self.token_name = "developer_auth"

        return token

# %% ../../nbs/client/95_DomoAuth.ipynb 47
@dataclass
class _DomoJupyter_Optional:
    def __post_init__(self):
        
        self.jupyter_token = self.jupyter_token or input( "jupyter token: # retrieve this by monitoring domo jupyter network traffic.  it is the Authorization header")
        self.service_location = self.service_location or input( "service_location:  # retrieve from domo jupyter env")
        self.service_prefix = self.service_prefix or input("service prefix: # retrieve from domo jupyter env")
                
        self._test_prereq()
        self.set_manual_login()
        
    def generate_auth_header(self, token: str) -> dict:
        self.auth_header = {"x-domo-authentication": token,
                            "authorization": f"Token {self.jupyter_token}"}

        return self.auth_header


@dataclass
class _DomoJupyter_Required:
    jupyter_token: str
    service_location: str
    service_prefix: str

    def get_jupyter_token_flow(self):
        """stub"""
        print('hello world i am a jupyter_token')

    def _test_prereq(self):
        if not self.jupyter_token:
            raise Exception("DomoJupyterAuth objects must have a jupyter_token")

        if not self.service_location:
            raise Exception("DomoJupyterAuth objects must have a service_location")

        if not self.service_prefix:
            raise Exception("DomoJupyterAuth objects must have a service_prefix")

        if not self.jupyter_token or not self.service_location or not self.service_prefix:
            raise Exception("DomoJupyterAuth objects must have jupyter_token, service_location and service_prefix")



# %% ../../nbs/client/95_DomoAuth.ipynb 48
@dataclass
class DomoJupyterAuth(_DomoJupyter_Optional,_DomoJupyter_Required ):
    """base class"""

# %% ../../nbs/client/95_DomoAuth.ipynb 50
@dataclass
class DomoJupyterFullAuth(
    _DomoJupyter_Optional, DomoFullAuth, _DomoJupyter_Required):

    @classmethod
    def convert_auth(cls, full_auth : DomoFullAuth, jupyter_token, service_location, service_prefix):
        """converts DomoFullAuth to DomoJupyterFullAuth
        i.e. adds DomoJupyter specific auth fields
        eventually can add DomoJupyter specific auth flow for generating auth token
        """
        return cls(domo_instance=full_auth.domo_instance,
                   domo_username=full_auth.domo_username,
                   domo_password=full_auth.domo_password,
                   jupyter_token=jupyter_token,
                   service_location=service_location,
                   service_prefix=service_prefix)

# %% ../../nbs/client/95_DomoAuth.ipynb 54
@dataclass
class DomoJupyterTokenAuth(
        _DomoJupyter_Optional, DomoTokenAuth, _DomoJupyter_Required):

    @classmethod
    def convert_auth(cls, token_auth: DomoTokenAuth, jupyter_token, service_location, service_prefix):
        """converts DomoTokenAuth to DomoJupyterTokenAuth
        i.e. adds DomoJupyter specific auth fields
        eventually can add DomoJupyter specific auth flow for generating auth token
        """
        return cls(domo_instance=token_auth.domo_instance,
                   domo_access_token=token_auth.domo_access_token,
                   jupyter_token=jupyter_token,
                   service_location=service_location,
                   service_prefix=service_prefix)


# %% ../../nbs/client/95_DomoAuth.ipynb 58
def test_is_jupyter_auth(auth: DomoJupyterAuth,
                         function_name=None,
                         required_auth_type_ls=[DomoJupyterFullAuth, DomoJupyterTokenAuth]):

    tb= lg.get_traceback()

    if auth.__class__.__name__ not in [auth_type.__name__ for auth_type in required_auth_type_ls]:
        raise InvalidAuthTypeError(
            function_name=tb.function_name, domo_instance=auth.domo_instance, required_auth_type_ls=required_auth_type_ls
        )

