<commit_before>#!/usr/bin/env python
#
#   PureResponseClient
#   Python API wrapper for PureResponse by Pure360
#   For internal use and public release
#
#   MIT License
#   Copyright (c) 2013 Triggered Messaging Ltd
#   Authored by <NAME>
#

import datetime
from time import strftime as strftime
import suds
from suds.client import Client as SudsPaint
import StringIO
import csv
import base64

class PureResponseClient(object):
    version = '0.1'
    
    api_username    = None
    api_password    = None
    api_context     = None
    api_client      = None
    
    class API:
        RPC_ENCODED_BRANDED     = 'http://paint.pure360.com/paint.pure360.com/ctrlPaint.wsdl'
        RPC_LITERAL_BRANDED     = 'http://paint.pure360.com/paint.pure360.com/ctrlPaintLiteral.wsdl'
        RPC_ENCODED_UNBRANDED   = 'http://emailapi.co.uk/emailapi.co.uk/ctrlPaint.wsdl'
        RPC_LITERAL_UNBRANDED   = 'http://emailapi.co.uk/emailapi.co.uk/ctrlPaintLiteral.wsdl'
    
    class TYPES:
        ARRAY   = 'paintArray'
        KVP     = 'paintKeyValuePair'
        
        class KEYS:
            VALUE   = 'value'
            KEY     = 'key'
            STRING  = 'str'
            ARRAY   = 'arr'
            PAIRS   = 'pairs'
    
    class BEAN_PROCESSES:
        SEARCH          = 'search'
        STORE           = 'store'
        CREATE          = 'create'
        AUTHENTICATE    = 'login'
        INVALIDATE      = 'logout'
    
    class BEAN_CLASSES:
        CAMPAIGN_DELIVERY   = 'campaign_delivery'
        CAMPAIGN_EMAIL      = 'campaign_email'
        CAMPAIGN_LIST       = 'campaign_list'
        CAMPAIGN_ONE_TO_ONE = 'campaign_one2one'
        CONTEXT             = 'context'
    
    class BEAN_TYPES:
        ENTITY  = 'bus_entity'
        FACADE  = 'bus_facade'
        SEARCH  = 'bus_search'
    
    class FIELDS:
        USERNAME        = 'userName'
        PASSWORD        = 'password'
        EMAIL           = 'email'
        EMAIL_COLUMN    = 'emailCol'
        MOBILE          = 'mobile'
        MOBILE_COLUMN   = 'mobileCol'
        MESSAGE_ID      = 'messageId'
        MESSAGE_NAME    = 'messageName'
        BEAN_ID         = 'beanId'
        LIST_IDS        = 'listIds'
        LIST_ID         = 'listId'
        LIST_NAME       = 'listName'
        DELIVERY_TIME   = 'deliveryDtTm'
        FOUND_DATA      = 'idData'
        RESULT          = 'result'
        RESULT_DATA     = 'resultData'
        MSG_MSG_NAME    = 'message_messageName'
        TO_ADDRESS      = 'toAddress'
        CUSTOM_DATA     = 'customData'
        UPLOAD_TYPE     = 'uploadTransactionType'
        PASTE_FILE      = 'pasteFile'
        FIELD_PARTIAL   = 'field'
        COLUMN_PARTIAL  = 'Col'
        NAME_PARTIAL    = 'Name'
        BASE64_PARTIAL  = '_base64'
    
    class VALUES:
        APPEND                  = 'APPEND'
        SUCCESS                 = 'success'
        SCHEDULING_UNIT         = 'minutes'
        SCHEDULING_DELAY        = 3
    
    class EXCEPTIONS:
        VALIDATION          = 'bean_exception_validation'
    
    class ERRORS:
        GENERIC             = 'ERROR_GENERIC'
        NOT_AUTHENTICATED   = 'ERROR_NOT_AUTHENTICATED'
        AUTH_PARAMS         = 'ERROR_AUTHENTICATION_PARAMETERS'
        AUTH_PROCESS        = 'ERROR_AUTHENTICATION_PROCESS'
        LIST_NOT_FOUND      = 'ERROR_LIST_NOT_FOUND'
        LIST_NOT_SAVED      = 'ERROR_LIST_NOT_SAVED'
        CONTACT_NOT_FOUND   = 'ERROR_CONTACT_NOT_FOUND'
        CAMPAIGN_NOT_FOUND  = 'ERROR_CAMPAIGN_NOT_FOUND'
        BEAN_NOT_CREATED    = 'ERROR_BEAN_NOT_CREATED'
        COULD_NOT_DELIVER   = 'ERROR_COULD_NOT_DELIVER'
        INVALID_PARAMS      = 'ERROR_INVALID_PARAMETERS'
    
    def __init__(self, api_version = API.RPC_LITERAL_UNBRANDED):
        self.api_client     = SudsPaint(api_version)
    
    def api_authenticate(self, api_username = '', api_password = ''):
        """
        Authenticate to receive a context key for use in API requests.
        Worth noting that there may be unexpected behaviour for special 
        characters, it is unclear how these should be handled.
        ----------------------------------------------
        @param api_username     - username.
        @param api_password     - password.
        """
        self.api_username = api_username.encode('utf-8')
        self.api_password = <PASSWORD>_password.encode('<PASSWORD>')
        if (not api_username) or (not api_password):
            raise Exception(PureResponseClient.ERRORS.AUTH_PARAMS)
        
        auth = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CONTEXT
          , PureResponseClient.BEAN_PROCESSES.AUTHENTICATE
          , {
                PureResponseClient.FIELDS.USERNAME : self.api_username
              , PureResponseClient.FIELDS.PASSWORD : <PASSWORD>
            }
        )
        if self._result_success(auth):
            self.api_context = self._get_bean_id(
                auth
              , PureResponseClient.BEAN_TYPES.ENTITY
              , PureResponseClient.BEAN_CLASSES.CONTEXT
            )
            return self._dict_ok(self.api_context)
        elif self._result_exception(auth, PureResponseClient.EXCEPTIONS.VALIDATION):
            return self._dict_err(PureResponseClient.ERRORS.AUTH_PARAMS, auth)
        else:
            return self._dict_err(PureResponseClient.ERRORS.AUTH_PROCESS, auth)
    
    def api_invalidate(self):
        self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CONTEXT
          , PureResponseClient.BEAN_PROCESSES.INVALIDATE
          , no_response = True
        )
        self.api_context = None
    
    def api_send_to_list(self, list_name, message_name, scheduling_delay = {
        VALUES.SCHEDULING_UNIT : VALUES.SCHEDULING_DELAY}):
        """
        Bulk send for email campaign to a contact list.
        ----------------------------------------------
        @param list_name        - name of a contact list in the system.
        @param message_name     - name of an email in the system.
        @param scheduling_delay - [optional] define how long the system 
                                  should wait before sending the campaign, 
                                  defaults to 3 minutes.
        """
        create = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_DELIVERY
          , PureResponseClient.BEAN_PROCESSES.CREATE
        )
        delivery_input = {
            PureResponseClient.FIELDS.BEAN_ID : self._get_bean_id(
                create
              , PureResponseClient.BEAN_TYPES.ENTITY
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_DELIVERY
            )
        }
        search_response = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
          , PureResponseClient.BEAN_PROCESSES.SEARCH
          , {PureResponseClient.FIELDS.LIST_NAME : list_name}
        )
        if self._result_success(search_response):
            found = self._get_found_data(
                search_response
              , PureResponseClient.BEAN_TYPES.SEARCH
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
            )
            if len(found) is not 0:
                delivery_input[PureResponseClient.FIELDS.LIST_IDS] = {
                    '0': found['0'].get(PureResponseClient.FIELDS.LIST_ID)
                }
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.ERROR_CAMPAIGN_NOT_FOUND
                  , self._response_data(search_response)
                )
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.GENERIC
              , self._response_data(search_response)
            )
        
        search_response = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_EMAIL
          , PureResponseClient.BEAN_PROCESSES.SEARCH
          , {PureResponseClient.FIELDS.MESSAGE_NAME : message_name}
        )
        
        if self._result_success(search_response):
            found = self._get_found_data(
                search_response
              , PureResponseClient.BEAN_TYPES.SEARCH
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_EMAIL
            )
            if len(found) is 1:
                delivery_input[
                    PureResponseClient.FIELDS.MESSAGE_ID
                ] = found['0'].get(PureResponseClient.FIELDS.MESSAGE_ID)
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.CAMPAIGN_NOT_FOUND
                  , self._response_data(search_response)
                )
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.GENERIC
              , self._response_data(search_response)
            )
        schedule_time = datetime.datetime.now() + datetime.timedelta(**scheduling_delay)
        schedule_time = schedule_time.strftime('%d/%m/%Y %H:%M')
        delivery_input[PureResponseClient.FIELDS.DELIVERY_TIME] = schedule_time
        
        response = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_DELIVERY
          , PureResponseClient.BEAN_PROCESSES.STORE
          , delivery_input
        )
        
        if self._result_success(response):
            return self._dict_ok(PureResponseClient.VALUES.SUCCESS)
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.COULD_NOT_DELIVER
              , self._response_data(response)
            )
    
    def api_send_to_contact(self, email_to, message_name, custom_data = None):
        """
        Send one to one email message.
        ----------------------------------------------
        @param email_to         - email address of intended recipient
        @param message_name     - name of email message in the system
        @param custom_data      - any desired merge tags, should match 
                                  fully with ones defined in the email 
                                  unless default values are available 
                                  in the contact list
        """
        process_data = {PureResponseClient.FIELDS.MSG_MSG_NAME : message_name}
        entity_data = {PureResponseClient.FIELDS.TO_ADDRESS : email_to}
        if custom_data is not None:
            entity_data[PureResponseClient.FIELDS.CUSTOM_DATA] = custom_data
        
        create = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_ONE_TO_ONE
          , PureResponseClient.BEAN_PROCESSES.CREATE
          , entity_data
          , process_data
        )
        
        if self._result_success(create):
            entity_data = {
                PureResponseClient.FIELDS.BEAN_ID : self._get_bean_id(
                    create
                  , PureResponseClient.BEAN_TYPES.ENTITY
                  , PureResponseClient.BEAN_CLASSES.CAMPAIGN_ONE_TO_ONE
                )
            }
            
            response = self.api_make_request(
                PureResponseClient.BEAN_TYPES.FACADE
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_ONE_TO_ONE
              , PureResponseClient.BEAN_PROCESSES.STORE
              , entity_data
            )
            
            if self._result_success(response):
                return self._dict_ok(PureResponseClient.VALUES.SUCCESS)
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.COULD_NOT_DELIVER
                  , self._response_data(response)
                )
        return self._dict_err(
            PureResponseClient.ERRORS.GENERIC
          , self._response_data(create)
        )
    
    def _api_append_contact_list(self, entity_data):
        """
        Internal use.
        Abstraction layer between built entity data 
        (self._api_add_contact_ambiguous) and sending 
        request (self.api_make_request).
        ----------------------------------------------
        @param entity_data      - information about the contacts 
                                  to append as well as general query 
                                  information
        """
        create = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
          , PureResponseClient.BEAN_PROCESSES.CREATE
        )
        if self._result_success(create):
            entity_data[PureResponseClient.FIELDS.BEAN_ID] = self._get_bean_id(
                create
              , PureResponseClient.BEAN_TYPES.ENTITY
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
            )
            response = self.api_make_request(
                PureResponseClient.BEAN_TYPES.FACADE
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
              , PureResponseClient.BEAN_PROCESSES.STORE
              , entity_data
            )
            
            if self._result_success(response):
                return self._dict_ok(PureResponseClient.VALUES.SUCCESS)
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.LIST_NOT_SAVED
                  , self._response_data(response)
                )
        return self._dict_err(
            PureResponseClient.ERRORS.GENERIC
          , self._response_data(create)
        )
    
    def _api_add_contact_ambiguous(self, list_name, contact_data):
        """
        Internal use.
        Abstraction layer between publically exposed functions 
        (self.api_add_contact, self.api_add_contacts) and request 
        handling (self._api_append_contact_list).
        ----------------------------------------------
        @param list_name        - name of contact list to append to.
        @param contact_data     - dictionary or list of dictionaries 
                                  containing contact data to append.
        """
        entity_data = {
            PureResponseClient.FIELDS.LIST_NAME     : list_name
          , PureResponseClient.FIELDS.UPLOAD_TYPE   : PureResponseClient.VALUES.APPEND
        }
        if isinstance(contact_data, list):
            paste_file = self._dictlist_to_csv(contact_data)
        else:
            paste_file = self._dict_to_csv(contact_data)
        entity_data[PureResponseClient.FIELDS.PASTE_FILE
            + PureResponseClient.FIELDS.BASE64_PARTIAL] = base64.b64encode(
            paste_file
        )
        entity_data = dict(
            entity_data
          , **self._build_contact_entity(paste_file)
        )
        return self._api_append_contact_list(entity_data)
        
    def api_add_contact(self, list_name, contact):
        """
        Add single contact to a given contact list.
        ----------------------------------------------
        @param list_name        - name of contact list to append to
        @param contact          - dictionary of contact data
        """
        return self._api_add_contact_ambiguous(list_name, contact)
    
    def api_add_contacts(self, list_name, contacts):
        """
        Add multiple contacts to a given contact list.
        The contacts parameter should be a list of dictionaries. 
        The dictionaries do not require matching key sets as a 
        master-list of keys is generated in the process of 
        producing a csv to upload.
        ----------------------------------------------
        @param list_name        - name of contact list to append to
        @param contacts         - list of dictionaries
        """
        return self._api_add_contact_ambiguous(list_name, contacts)
    
    def api_make_request(self, bean_type, bean_class, bean_process
      , entity_data = None, process_data = None, no_response = False):
        if self.api_context or (bean_process is PureResponseClient.BEAN_PROCESSES.AUTHENTICATE):
            api_context = self.api_context or suds.null()
            response    = self.api_client.service.handleRequest(
                            api_context
                          , bean_type + '_' + bean_class
                          , bean_process
                          , self._dict_to_ptarr(entity_data)
                          , self._dict_to_ptarr(process_data)
                        )
            if no_response:
                return True
            else:
                return self._ptarr_to_dict(response)
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.NOT_AUTHENTICATED
              , None
            )
    
    def _response_data(self, response_dict, bean_type = None
        , bean_class = None, field = FIELDS.RESULT_DATA):
        if (bean_type is not None) and (bean_class is not None):
            return response_dict[field][bean_type + '_' + bean_class]
        elif (bean_type is not None) or (bean_class is not None):
            raise Exception(PureResponseClient.ERRORS.INVALID_PARAMS)
        else:
            return response_dict[field]
    
    def _get_result(self, response):
        return self._response_data(response, field=PureResponseClient.FIELDS.RESULT)
    
    def _result_success(self, response):
        return self._get_result(response) == PureResponseClient.VALUES.SUCCESS
    
    def _get_bean_id(self, response, bean_type, bean_class):
        return self._response_data(
            response
          , bean_type
          , bean_class
        ).get(PureResponseClient.FIELDS.BEAN_ID)
    
    def _get_found_data(self, response, bean_type, bean_class):
        return self._response_data(
            response
          , bean_type
          , bean_class
        ).get(PureResponseClient.FIELDS.FOUND_DATA)
    
    def _result_exception(self, response, exception):
        return self._get_result(response) is exception
    
    def _dict_ok(self, result):
        return {'ok' : True, 'result': result}
    
    def _dict_err(self, error, meta):
        return {'ok' : False, 'result' : error, 'meta' : meta}
    
    def _unicode_exceptions(self, key):
        exceptions = [
            PureResponseClient.FIELDS.BEAN_ID
          , PureResponseClient.FIELDS.MESSAGE_ID
          , PureResponseClient.FIELDS.LIST_ID
        ]
        if key.isdigit():
            return True
        else:
            for field in exceptions:
                if key == unicode(field):
                    return True
            return False
    
    def _dict_to_ptarr(self, dict_):
        """
        Internal use.
        Convert dictionaries to 'paintArray' objects using suds.
        Works some encoding magic in the process in order to be 
        compliant with the expectations and requirements of the 
        API while attempting to maintain the integrity of the 
        data content.
        ----------------------------------------------
        @param dict_        - dictionary of data to convert.
        """
        if not dict_:
            return suds.null()
        arr_ = self.api_client.factory.create(PureResponseClient.TYPES.ARRAY)
        for key_ in dict_:<commit_msg>Updated ppp<commit_after>#!/usr/bin/env python
#
#   PureResponseClient
#   Python API wrapper for PureResponse by Pure360
#   For internal use and public release
#
#   MIT License
#   Copyright (c) 2013 Triggered Messaging Ltd
#   Authored by <NAME>
#

import datetime
from time import strftime as strftime
import suds
from suds.client import Client as SudsPaint
import StringIO
import csv
import base64

class PureResponseClient(object):
    version = '0.1'
    
    api_username    = None
    api_password    = None
    api_context     = None
    api_client      = None
    
    class API:
        RPC_ENCODED_BRANDED     = 'http://paint.pure360.com/paint.pure360.com/ctrlPaint.wsdl'
        RPC_LITERAL_BRANDED     = 'http://paint.pure360.com/paint.pure360.com/ctrlPaintLiteral.wsdl'
        RPC_ENCODED_UNBRANDED   = 'http://emailapi.co.uk/emailapi.co.uk/ctrlPaint.wsdl'
        RPC_LITERAL_UNBRANDED   = 'http://emailapi.co.uk/emailapi.co.uk/ctrlPaintLiteral.wsdl'
    
    class TYPES:
        ARRAY   = 'paintArray'
        KVP     = 'paintKeyValuePair'
        
        class KEYS:
            VALUE   = 'value'
            KEY     = 'key'
            STRING  = 'str'
            ARRAY   = 'arr'
            PAIRS   = 'pairs'
    
    class BEAN_PROCESSES:
        SEARCH          = 'search'
        STORE           = 'store'
        CREATE          = 'create'
        LOAD            = 'load'
        REMOVE          = 'remove'
        AUTHENTICATE    = 'login'
        INVALIDATE      = 'logout'
    
    class BEAN_CLASSES:
        CAMPAIGN_DELIVERY   = 'campaign_delivery'
        CAMPAIGN_EMAIL      = 'campaign_email'
        CAMPAIGN_LIST       = 'campaign_list'
        CAMPAIGN_ONE_TO_ONE = 'campaign_one2one'
        CONTEXT             = 'context'
    
    class BEAN_TYPES:
        ENTITY  = 'bus_entity'
        FACADE  = 'bus_facade'
        SEARCH  = 'bus_search'
    
    class FIELDS:
        USERNAME            = 'userName'
        PASSWORD            = 'password'
        EMAIL               = 'email'
        EMAIL_COLUMN        = 'emailCol'
        MOBILE              = 'mobile'
        MOBILE_COLUMN       = 'mobileCol'
        MESSAGE_ID          = 'messageId'
        MESSAGE_NAME        = 'messageName'
        BEAN_ID             = 'beanId'
        LIST_IDS            = 'listIds'
        LIST_ID             = 'listId'
        LIST_NAME           = 'listName'
        DELIVERY_TIME       = 'deliveryDtTm'
        FOUND_DATA          = 'idData'
        RESULT              = 'result'
        RESULT_DATA         = 'resultData'
        MSG_MSG_NAME        = 'message_messageName'
        TO_ADDRESS          = 'toAddress'
        CUSTOM_DATA         = 'customData'
        UPLOAD_TYPE         = 'uploadTransactionType'
        UPLOAD_NOTIFY_URI   = 'uploadFileNotifyEmail'
        PASTE_FILE          = 'pasteFile'
        FIELD_PARTIAL       = 'field'
        COLUMN_PARTIAL      = 'Col'
        NAME_PARTIAL        = 'Name'
        BASE64_PARTIAL      = '_base64'
    
    class VALUES:
        APPEND                  = 'APPEND'
        SUCCESS                 = 'success'
        SCHEDULING_UNIT         = 'minutes'
        SCHEDULING_DELAY        = 3
    
    class EXCEPTIONS:
        VALIDATION          = 'bean_exception_validation'
    
    class ERRORS:
        GENERIC             = 'ERROR_GENERIC'
        NOT_AUTHENTICATED   = 'ERROR_NOT_AUTHENTICATED'
        AUTH_PARAMS         = 'ERROR_AUTHENTICATION_PARAMETERS'
        AUTH_PROCESS        = 'ERROR_AUTHENTICATION_PROCESS'
        LIST_NAME_EXISTS    = 'ERROR_LIST_NAME_EXISTS'
        LIST_NOT_FOUND      = 'ERROR_LIST_NOT_FOUND'
        LIST_NOT_SAVED      = 'ERROR_LIST_NOT_SAVED'
        LIST_NOT_REMOVED    = 'ERROR_LIST_NOT_REMOVED'
        CONTACT_NOT_FOUND   = 'ERROR_CONTACT_NOT_FOUND'
        CAMPAIGN_NOT_FOUND  = 'ERROR_CAMPAIGN_NOT_FOUND'
        BEAN_NOT_CREATED    = 'ERROR_BEAN_NOT_CREATED'
        COULD_NOT_DELIVER   = 'ERROR_COULD_NOT_DELIVER'
        INVALID_PARAMS      = 'ERROR_INVALID_PARAMETERS'
    
    def __init__(self, api_version = API.RPC_LITERAL_UNBRANDED):
        self.api_client     = SudsPaint(api_version)
    
    def api_authenticate(self, api_username = '', api_password = ''):
        """
        Authenticate to receive a context key for use in API requests.
        Worth noting that there may be unexpected behaviour for special 
        characters, it is unclear how these should be handled.
        ----------------------------------------------
        @param api_username     - username.
        @param api_password     - password.
        """
        self.api_username = api_username.encode('utf-8')
        self.api_password = <PASSWORD>.encode('<PASSWORD>')
        if (not api_username) or (not api_password):
            raise Exception(PureResponseClient.ERRORS.AUTH_PARAMS)
        
        auth = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CONTEXT
          , PureResponseClient.BEAN_PROCESSES.AUTHENTICATE
          , {
                PureResponseClient.FIELDS.USERNAME : self.api_username
              , PureResponseClient.FIELDS.PASSWORD : self.api_password
            }
        )
        if self._result_success(auth):
            self.api_context = self._get_bean_id(
                auth
              , PureResponseClient.BEAN_TYPES.ENTITY
              , PureResponseClient.BEAN_CLASSES.CONTEXT
            )
            return self._dict_ok(self.api_context)
        elif self._result_exception(auth, PureResponseClient.EXCEPTIONS.VALIDATION):
            return self._dict_err(PureResponseClient.ERRORS.AUTH_PARAMS, auth)
        else:
            return self._dict_err(PureResponseClient.ERRORS.AUTH_PROCESS, auth)
    
    def api_invalidate(self):
        self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CONTEXT
          , PureResponseClient.BEAN_PROCESSES.INVALIDATE
          , no_response = True
        )
        self.api_context = None
    
    def api_send_to_list(self, list_name, message_name, scheduling_delay = {
        VALUES.SCHEDULING_UNIT : VALUES.SCHEDULING_DELAY}):
        """
        Bulk send for email campaign to a contact list.
        ----------------------------------------------
        @param list_name        - name of a contact list in the system.
        @param message_name     - name of an email in the system.
        @param scheduling_delay - [optional] define how long the system 
                                  should wait before sending the campaign, 
                                  defaults to 3 minutes.
        """
        create = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_DELIVERY
          , PureResponseClient.BEAN_PROCESSES.CREATE
        )
        delivery_input = {
            PureResponseClient.FIELDS.BEAN_ID : self._get_bean_id(
                create
              , PureResponseClient.BEAN_TYPES.ENTITY
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_DELIVERY
            )
        }
        search_response = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
          , PureResponseClient.BEAN_PROCESSES.SEARCH
          , {PureResponseClient.FIELDS.LIST_NAME : list_name}
        )
        if self._result_success(search_response):
            found = self._get_found_data(
                search_response
              , PureResponseClient.BEAN_TYPES.SEARCH
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
            )
            if len(found) is not 0:
                delivery_input[PureResponseClient.FIELDS.LIST_IDS] = {
                    '0': found['0'].get(PureResponseClient.FIELDS.LIST_ID)
                }
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.ERROR_CAMPAIGN_NOT_FOUND
                  , self._response_data(search_response)
                )
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.GENERIC
              , self._response_data(search_response)
            )
        
        search_response = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_EMAIL
          , PureResponseClient.BEAN_PROCESSES.SEARCH
          , {PureResponseClient.FIELDS.MESSAGE_NAME : message_name}
        )
        
        if self._result_success(search_response):
            found = self._get_found_data(
                search_response
              , PureResponseClient.BEAN_TYPES.SEARCH
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_EMAIL
            )
            if len(found) is 1:
                delivery_input[
                    PureResponseClient.FIELDS.MESSAGE_ID
                ] = found['0'].get(PureResponseClient.FIELDS.MESSAGE_ID)
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.CAMPAIGN_NOT_FOUND
                  , self._response_data(search_response)
                )
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.GENERIC
              , self._response_data(search_response)
            )
        schedule_time = datetime.datetime.now() + datetime.timedelta(**scheduling_delay)
        schedule_time = schedule_time.strftime('%d/%m/%Y %H:%M')
        delivery_input[PureResponseClient.FIELDS.DELIVERY_TIME] = schedule_time
        
        response = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_DELIVERY
          , PureResponseClient.BEAN_PROCESSES.STORE
          , delivery_input
        )
        
        if self._result_success(response):
            return self._dict_ok(PureResponseClient.VALUES.SUCCESS)
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.COULD_NOT_DELIVER
              , self._response_data(response)
            )
    
    def api_send_to_contact(self, email_to, message_name, custom_data = None):
        """
        Send one to one email message.
        ----------------------------------------------
        @param email_to         - email address of intended recipient
        @param message_name     - name of email message in the system
        @param custom_data      - any desired merge tags, should match 
                                  fully with ones defined in the email 
                                  unless default values are available 
                                  in the contact list
        """
        process_data = {PureResponseClient.FIELDS.MSG_MSG_NAME : message_name}
        entity_data = {PureResponseClient.FIELDS.TO_ADDRESS : email_to}
        if custom_data is not None:
            entity_data[PureResponseClient.FIELDS.CUSTOM_DATA] = custom_data
        
        create = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_ONE_TO_ONE
          , PureResponseClient.BEAN_PROCESSES.CREATE
          , entity_data
          , process_data
        )
        
        if self._result_success(create):
            entity_data = {
                PureResponseClient.FIELDS.BEAN_ID : self._get_bean_id(
                    create
                  , PureResponseClient.BEAN_TYPES.ENTITY
                  , PureResponseClient.BEAN_CLASSES.CAMPAIGN_ONE_TO_ONE
                )
            }
            
            response = self.api_make_request(
                PureResponseClient.BEAN_TYPES.FACADE
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_ONE_TO_ONE
              , PureResponseClient.BEAN_PROCESSES.STORE
              , entity_data
            )
            
            if self._result_success(response):
                return self._dict_ok(PureResponseClient.VALUES.SUCCESS)
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.COULD_NOT_DELIVER
                  , self._response_data(response)
                )
        return self._dict_err(
            PureResponseClient.ERRORS.GENERIC
          , self._response_data(create)
        )
    
    """
    OFFER TO EITHER DO LIST OF DICTS WITH DATA UPLOAD
    OR SIMPLY DO THE FIELDS WITH AN EMPTY CSV, FIELDS BEING DONE AS A SINGLE LIST OF STRINGS
    """
    def api_create_contact_list(self, list_name, list_data
        , notify_uri = None, overwrite_existing = False):
        search_response = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
          , PureResponseClient.BEAN_PROCESSES.SEARCH
          , {PureResponseClient.FIELDS.LIST_NAME : list_name}
        )
        if self._result_success(search_response):
            found = self._get_found_data(
                search_response
              , PureResponseClient.BEAN_TYPES.SEARCH
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
            )
            if len(found) is 0:
                return self._api_new_contact_list_helper(list_name, list_data, notify_uri)
            elif overwrite_existing:
                remove_response = self._api_remove_contact_list_helper(list_name, found)
                # KEY ERRORS going after resultData in remove_response
                if self._result_success(remove_response):
                    return self._api_new_contact_list_helper(list_name, list_data, notify_uri)
                else:
                    return remove_response
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.LIST_NAME_EXISTS
                  , self._response_data(search_response)
                )
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.GENERIC
              , self._response_data(search_response)
            )
    
    def _api_new_contact_list_helper(self, list_name, list_data, notify_uri):
        create = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
          , PureResponseClient.BEAN_PROCESSES.CREATE
        )
        if self._result_success(create):
            entity_data = {
                PureResponseClient.FIELDS.UPLOAD_NOTIFY_URI : notify_uri
              , PureResponseClient.FIELDS.LIST_NAME         : list_name
              , PureResponseClient.FIELDS.BEAN_ID           : self._get_bean_id(
                    create
                  , PureResponseClient.BEAN_TYPES.ENTITY
                  , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
                )
            }
            
            paste_file = self._dictlist_to_csv(list_data)
            entity_data[
                PureResponseClient.FIELDS.PASTE_FILE
              + PureResponseClient.FIELDS.BASE64_PARTIAL
            ] = base64.b64encode(paste_file)
            
            entity_data = dict(
                entity_data
              , **self._build_contact_entity(
                    paste_file
                )
            )
            response = self.api_make_request(
                PureResponseClient.BEAN_TYPES.FACADE
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
              , PureResponseClient.BEAN_PROCESSES.STORE
              , entity_data
            )
            if self._result_success(response):
                return self._dict_ok(PureResponseClient.VALUES.SUCCESS)
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.LIST_NOT_SAVED
                  , self._response_data(response)
                )
        return self._dict_err(
            PureResponseClient.ERRORS.BEAN_NOT_CREATED
          , self._response_data(search_response)
        )
    
    def _api_remove_contact_list_helper(self, list_name, found):
        for key in found:
            entity_data = found[key]
            load_response = self.api_make_request(
                PureResponseClient.BEAN_TYPES.FACADE
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
              , PureResponseClient.BEAN_PROCESSES.LOAD
              , entity_data
            )
            
            if not self._result_success(load_response):
                continue
            
            load_output = self._response_data(
                load_response
              , PureResponseClient.BEAN_TYPES.ENTITY
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
            )
            
            if (unicode(load_output.get(PureResponseClient.FIELDS.LIST_NAME)) == unicode(list_name)):
                entity_data = {
                    PureResponseClient.FIELDS.BEAN_ID : load_output[
                        PureResponseClient.FIELDS.BEAN_ID
                    ]
                }
                
                return self.api_make_request(
                    PureResponseClient.BEAN_TYPES.FACADE
                  , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
                  , PureResponseClient.BEAN_PROCESSES.REMOVE
                  , entity_data
                )
        return self._dict_err(
            PureResponseClient.ERRORS.LIST_NOT_FOUND
          , found
        )
    
    def _api_append_contact_list(self, entity_data):
        """
        Internal use.
        Abstraction layer between built entity data 
        (self._api_add_contact_ambiguous) and sending 
        request (self.api_make_request).
        ----------------------------------------------
        @param entity_data      - information about the contacts 
                                  to append as well as general query 
                                  information
        """
        create = self.api_make_request(
            PureResponseClient.BEAN_TYPES.FACADE
          , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
          , PureResponseClient.BEAN_PROCESSES.CREATE
        )
        if self._result_success(create):
            entity_data[PureResponseClient.FIELDS.BEAN_ID] = self._get_bean_id(
                create
              , PureResponseClient.BEAN_TYPES.ENTITY
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
            )
            response = self.api_make_request(
                PureResponseClient.BEAN_TYPES.FACADE
              , PureResponseClient.BEAN_CLASSES.CAMPAIGN_LIST
              , PureResponseClient.BEAN_PROCESSES.STORE
              , entity_data
            )
            
            if self._result_success(response):
                return self._dict_ok(PureResponseClient.VALUES.SUCCESS)
            else:
                return self._dict_err(
                    PureResponseClient.ERRORS.LIST_NOT_SAVED
                  , self._response_data(response)
                )
        return self._dict_err(
            PureResponseClient.ERRORS.GENERIC
          , self._response_data(create)
        )
    
    def _api_add_contact_ambiguous(self, list_name, contact_data):
        """
        Internal use.
        Abstraction layer between publically exposed functions 
        (self.api_add_contact, self.api_add_contacts) and request 
        handling (self._api_append_contact_list).
        ----------------------------------------------
        @param list_name        - name of contact list to append to.
        @param contact_data     - dictionary or list of dictionaries 
                                  containing contact data to append.
        """
        entity_data = {
            PureResponseClient.FIELDS.LIST_NAME     : list_name
          , PureResponseClient.FIELDS.UPLOAD_TYPE   : PureResponseClient.VALUES.APPEND
        }
        if isinstance(contact_data, list):
            paste_file = self._dictlist_to_csv(contact_data)
        else:
            paste_file = self._dict_to_csv(contact_data)
        entity_data[
            PureResponseClient.FIELDS.PASTE_FILE
          + PureResponseClient.FIELDS.BASE64_PARTIAL
        ] = base64.b64encode(paste_file)
        entity_data = dict(
            entity_data
          , **self._build_contact_entity(paste_file)
        )
        print paste_file
        print self._dict_to_ptarr(entity_data)
        return self._api_append_contact_list(entity_data)
        
    def api_add_contact(self, list_name, contact):
        """
        Add single contact to a given contact list.
        ----------------------------------------------
        @param list_name        - name of contact list to append to
        @param contact          - dictionary of contact data
        """
        return self._api_add_contact_ambiguous(list_name, contact)
    
    def api_add_contacts(self, list_name, contacts):
        """
        Add multiple contacts to a given contact list.
        The contacts parameter should be a list of dictionaries. 
        The dictionaries do not require matching key sets as a 
        master-list of keys is generated in the process of 
        producing a csv to upload.
        ----------------------------------------------
        @param list_name        - name of contact list to append to
        @param contacts         - list of dictionaries
        """
        return self._api_add_contact_ambiguous(list_name, contacts)
    
    def api_make_request(self, bean_type, bean_class, bean_process
      , entity_data = None, process_data = None, no_response = False):
        if self.api_context or (bean_process is PureResponseClient.BEAN_PROCESSES.AUTHENTICATE):
            api_context = self.api_context or suds.null()
            response    = self.api_client.service.handleRequest(
                            api_context
                          , bean_type + '_' + bean_class
                          , bean_process
                          , self._dict_to_ptarr(entity_data)
                          , self._dict_to_ptarr(process_data)
                        )
            if no_response:
                return True
            else:
                return self._ptarr_to_dict(response)
        else:
            return self._dict_err(
                PureResponseClient.ERRORS.NOT_AUTHENTICATED
              , None
            )
    
    def _response_data(self, response_dict, bean_type = None
        , bean_class = None, field = FIELDS.RESULT_DATA):
        if (bean_type is not None) and (bean_class is not None):
            #print field, bean_type, bean_class
            #print response_dict
            return response_dict[field][bean_type + '_' + bean_class]
        elif (bean_type is not None) or (bean_class is not None):
            raise Exception(PureResponseClient.ERRORS.INVALID_PARAMS)
        else:
            return response_dict[field]
    
    def _get_result(self, response):
        return self._response_data(response, field=PureResponseClient.FIELDS.RESULT)
    
    def _result_success(self, response):
        return self._get_result(response) == PureResponseClient.VALUES.SUCCESS
    
    def _get_bean_id(self, response, bean_type, bean_class):
        return self._response_data(
            response
          , bean_type
          , bean_class
        ).get(PureResponseClient.FIELDS.BEAN_ID)
    
    def _get_found_data(self, response, bean_type, bean_class):
        return self._response_data(
            response
          , bean_type
          , bean_class
        ).get(PureResponseClient.FIELDS.FOUND_DATA)
    
    def _result_exception(self, response, exception):
        return self._get_result(response) is exception
    
    def _dict_ok(self, result = VALUES.SUCCESS):
        return {'ok' : True, 'result': result}
    
    def _dict_err(self, error = ERRORS.GENERIC, meta = None):
        return {'ok' : False, 'result' : error, 'meta' : meta}
    
    def _unicode_exceptions(self, key):
        exceptions = [
            PureResponseClient.FIELDS.BEAN_ID
          , PureResponseClient.FIELDS.MESSAGE_ID
          , PureResponseClient.FIELDS.LIST_ID
        ]
        if key.isdigit():
            return True
        else:
            for field in exceptions:
                if key == unicode(field):
                    return True
            return False
    
    def _dict_to_ptarr(self, dict_):
        """
        Internal use.
        Convert dictionaries to 'paintArray' objects using suds.
        Works some encoding magic in the process in order to be 
        compliant with the expectations and requirements of the 
        API while attempting to maintain the integrity of the 
        data content.
        ----------------------------------------------
        @param dict_        - dictionary of data to convert.
        """
        if not dict_:
            return suds.null()
        arr_ = self.api_client.factory.create(PureResponseClient.TYPES.ARRAY)
        for key_ in dict_:
{'fraction_non_alphanumeric': 0.07977188711567938, 'fraction_numerical': 0.00205121269497791, 'mean_word_length': 1.6565868263473054, 'pattern_counts': {'":': 0, '<': 10, '<?xml version=': 0, '>': 10, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 16, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1623899', 'n_tokens_mistral': 12895, 'n_tokens_neox': 12247, 'n_words': 2192}