Core

Base classes and routes for authentication, and dealing with api responses.

Features or classes are implemented as separate notebooks and then exported into their own module folder (see default_exp line in each .ipynb file)

Implementations, take a set of features and create a use case or project - ex.back up code engine and custom app (features) into appdb collections (feature)


source

get_jupyter_account

 get_jupyter_account (account_name, domojupyter_fn:Callable)
Exported source
def get_jupyter_account(account_name, domojupyter_fn: Callable):
    creds_json = None
    i = 0

    while not creds_json:
        try:
            creds_json = json.loads(
                domojupyter_fn.get_account_property_value(account_name, "credentials")
            )

        except Exception as e:
            print(f"account error - retrying {e}")
            i = i + 1
            if i > 12:
                return
            time.sleep(5)

    return creds_json

handling authentication


source

DomoAuth

 DomoAuth (domo_instance:str, username:str=None, password:str=None,
           access_token:str=None, session_token:str=None)
Exported source
@dataclass
class DomoAuth:
    domo_instance: str

    username: str = None
    password: str = field(repr=False, default=None)
    access_token: str = field(repr=False, default=None)
    session_token: str = field(repr=False, default=None)

    @classmethod
    def from_creds_json(
        cls,
        creds_json: dict,
    ):
        username = creds_json.get("DOMO_USERNAME")
        password = creds_json.get("DOMO_PASSWORD")

        access_token = creds_json.get("accessToken") or creds_json.get("ACCESS_TOKEN")

        domo_instance = creds_json.get("instance_url")

        if username and password:
            return cls(
                username=username,
                password=password,
                access_token=access_token,
                domo_instance=domo_instance,
            )

    def generate_request_headers(self):
        headers = {
            "Accept": "application/json",
            "Content-Type": "application/json",
        }

        if self.session_token:
            headers.update({"x-domo-authentication": self.session_token})
            return headers

        if self.access_token:
            headers.update({"x-domo-developer-token": self.access_token})
            return headers

        raise Exception(
            "generate_request_headers: unable to authenticate request with provided Auth"
        )

    def who_am_i(self, return_raw: bool = False):
        """identify which credentials are being used in this Auth Object (useful for access_token based authentication)"""

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

        res = json.loads(
            requests.request(
                method="GET", headers=self.generate_request_headers(), url=url
            ).text
        )

        if return_raw:
            return res

        self.username = res["emailAddress"]

        return res

sample DomoAuth

auth = DomoAuth(
    domo_instance=os.environ["DOMO_INSTANCE"],
    access_token=os.environ["DOMO_ACCESS_TOKEN"],
)

print(auth.who_am_i(return_raw=False))
auth
{'id': 1893952720, 'invitorUserId': 587894148, 'displayName': 'Jae Wilson1', 'department': 'Business Improvement', 'userName': 'jae@onyxreporting.com', 'emailAddress': 'jae@datacrew.space', 'avatarKey': 'c605f478-0cd2-4451-9fd4-d82090b71e66', 'accepted': True, 'userType': 'USER', 'modified': 1721847526952, 'created': 1588960518, 'active': True, 'pending': False, 'anonymous': False, 'systemUser': False}
DomoAuth(domo_instance='domo-community', username='jae@datacrew.space')

sending requests


source

ResponseGetData

 ResponseGetData (auth:__main__.DomoAuth, response:dict, is_success:bool,
                  status:int, download_path:str=None)
Exported source
class API_Exception(Exception):

    def __init__(self, res, message: str = None):
        message = message or ""
        base = f" || {str(res.status)} - {res.response.get('message') or res.response.get('statusReason')} || {res.auth.domo_instance}"
        message += base

        super().__init__(message)


class Class_Exception(Exception):

    def __init__(self, cls, auth=None, message: str = None):
        cls_instance = cls

        if hasattr(cls, "__cls__"):
            cls_instance = cls
            cls = cls.__cls__

        domo_instance = {
            (cls_instance and cls_instance.auth.domo_instance)
            or (auth and auth.domo_instance)
        }

        message = f"{message or 'error'} || {cls.__name__}"

        if domo_instance:
            message = f"{message} || {domo_instance}"

        super().__init__(message)


@dataclass
class ResponseGetData:
    auth: DomoAuth = field(repr=False)
    response: dict
    is_success: bool
    status: int
    download_path: str = None

    @classmethod
    def from_response(cls, res, auth: DomoAuth):

        try:
            if res.text == "" or not res.text:
                return cls(
                    response=res.text,
                    is_success=res.ok,
                    status=res.status_code,
                    auth=auth,
                )

            return cls(
                response=json.loads(res.text),
                is_success=res.ok,
                status=res.status_code,
                auth=auth,
            )

        except Exception as e:
            print({"rgd.from_response": {"text": res.text, "err": e}})

            raise e

    @staticmethod
    def _write_stream(res: requests.Response, file_path: str, stream_chunks=8192):

        dmut.upsert_folder(file_path)

        with open(file_path, "wb") as fd:
            for chunk in res.iter_content(stream_chunks):
                fd.write(chunk)

        print("done writing stream")
        return True

    @staticmethod
    def read_stream(download_path):
        with open(download_path, "rb") as f:
            return f.read()

    @classmethod
    def from_stream(cls, res: requests.Response, download_path: str, auth: DomoAuth):

        if not res.ok:
            return cls.from_response(res=res, auth=auth)

        cls._write_stream(res, download_path)

        return cls(
            response=True if cls.read_stream(download_path) else False,
            is_success=res.ok,
            status=res.status_code,
            auth=auth,
            download_path=download_path,
        )

source

Class_Exception

 Class_Exception (cls, auth=None, message:str=None)

Common base class for all non-exit exceptions.


source

API_Exception

 API_Exception (res, message:str=None)

Common base class for all non-exit exceptions.


source

domo_api_request

 domo_api_request (auth:__main__.DomoAuth, endpoint, request_type,
                   params=None, headers=None, body=None,
                   return_raw:bool=False, debug_api:bool=False, timeout=3)
Exported source
def domo_api_request(
    auth: DomoAuth,
    endpoint,
    request_type,
    params=None,
    headers=None,
    body=None,
    return_raw: bool = False,
    debug_api: bool = False,
    timeout=3,
):

    url = f"https://{auth.domo_instance}.domo.com{endpoint}"

    headers = headers or {}

    headers = {**auth.generate_request_headers(), **headers}

    if debug_api:

        print("🐛 debugging domo_api_request")
        pprint(
            {
                "url": url,
                "headers": headers,
                "request_type": request_type,
                "params": params,
                "body": body,
            }
        )

    if request_type.lower() == "post":
        response = requests.post(
            url, json=body, headers=headers, params=params, timeout=timeout
        )

    elif request_type.lower() == "get":
        response = requests.get(url, headers=headers, params=params, timeout=timeout)

    else:
        raise Exception(
            f'domo_api_request method "{request_type.lower()}" not implemented yet.'
        )

    if return_raw:
        return response

    return ResponseGetData.from_response(res=response, auth=auth)

source

looper

 looper (auth:__main__.DomoAuth, arr_fn:Callable, endpoint:str,
         request_type='GET', params:dict=None, body:dict=None, offset=0,
         limit=50, offset_params:dict=None,
         offset_params_is_header:bool=False, debug_api:bool=False,
         debug_loop:bool=False, return_raw:bool=False)
Type Default Details
auth DomoAuth
arr_fn Callable
endpoint str
request_type str GET
params dict None
body dict None
offset int 0
limit int 50
offset_params dict None format {“offset” : <> , “limit” : <>}
offset_params_is_header bool False should offset parameters be passed in the header or body?
debug_api bool False
debug_loop bool False
return_raw bool False will break the looper after the first request and ignore the array processing step.
Exported source
looper_offset_params = {
    "offset": "offset",
    "limit": "limit",
}  # what are the offset parameters called that handle pagination?


def looper(
    auth: DomoAuth,
    arr_fn: Callable,
    endpoint: str,
    request_type="GET",
    params: dict = None,
    body: dict = None,
    offset=0,
    limit=50,
    offset_params: dict = None,  # format {"offset" : <<value>> , "limit" : <<value>>}
    offset_params_is_header: bool = False,  # should offset parameters be passed in the header or body?
    debug_api: bool = False,
    debug_loop: bool = False,
    return_raw: bool = False,  # will break the looper after the first request and ignore the array processing step.
):
    params = params or {}
    body = body or {}
    offset_params = offset_params or looper_offset_params

    final_array = []
    keep_looping = True

    while keep_looping:
        new_offset = {
            offset_params["offset"]: offset,
            offset_params["limit"]: limit,
        }
        if offset_params_is_header:
            params = deepcopy({**params, **new_offset})

        else:
            body = {**body, **new_offset}

        res = domo_api_request(
            auth=auth,
            endpoint=endpoint,
            request_type=request_type,
            params=params,
            debug_api=debug_api,
            body=body,
        )

        if res.status == 429:
            print("sleeping in timeout")
            time.sleep(10)
            debug_loop = True
            keep_looping = True

        elif not res.is_success or return_raw:
            return res

        else:
            new_array = arr_fn(res)

            if debug_loop:
                pprint("🔁 debug_loop")
                pprint(
                    {
                        "params": params,
                        "body": body,
                        # "new_array": new_array[0:1]
                    }
                )

            if not new_array or len(new_array) == 0:
                keep_looping = False

            if len(new_array) < limit:
                keep_looping = False

            final_array += new_array

            offset += limit

    res.response = final_array

    return res

source

domo_api_stream_request

 domo_api_stream_request (auth:__main__.DomoAuth, endpoint, request_type,
                          download_path, params=None, headers=None,
                          debug_api:bool=False, timeout=3)
Exported source
def domo_api_stream_request(
    auth: DomoAuth,
    endpoint,
    request_type,
    download_path,
    params=None,
    headers=None,
    debug_api: bool = False,
    timeout=3,
):

    url = f"https://{auth.domo_instance}.domo.com{endpoint}"

    headers = headers or {}

    headers = {**auth.generate_request_headers(), **headers}

    if debug_api:

        print("debugging domo_api_request")

        pprint(
            {
                "url": url,
                "headers": headers,
                "request_type": request_type,
                "params": params,
            }
        )

    res = requests.get(
        url=url, headers=headers, params=params, timeout=timeout, stream=True
    )

    if not res.ok:
        return ResponseGetData.from_response(res=res, auth=auth)

    return ResponseGetData.from_stream(res=res, download_path=download_path, auth=auth)