From 72eacc97c9f94a178919ade753d5766b7d893fc4 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 2 Jan 2020 15:42:45 +0000 Subject: [PATCH 1/3] Case 8603 - Moving changes into bronto-python repo --- bronto/client.py | 283 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 237 insertions(+), 46 deletions(-) diff --git a/bronto/client.py b/bronto/client.py index d7c9409..590442c 100644 --- a/bronto/client.py +++ b/bronto/client.py @@ -1,18 +1,31 @@ +import re + import six -from suds import WebFault import suds.client +from suds import WebFault API_ENDPOINT = 'https://api.bronto.com/v4?wsdl' +class BrontoException(Exception): + def __init__(self, message, code=None): + # try extract the status code with regex + if not code: + code = re.search('\d{3}', message) + try: + code = int(message[code.regs[0][0]:code.regs[0][1]]) + except Exception as e: + code = None + self.message = message + self.code = code + -class BrontoError(Exception): - pass class Client(object): _valid_contact_fields = ['email', 'mobileNumber', 'status', 'msgPref', 'source', 'customSource', 'listIds', 'fields', - 'SMSKeywordIDs'] + 'SMSKeywordIDs',] #'first_name', 'last_name', 'cart_html'] + _valid_order_fields = ['id', 'email', 'contactId', 'products', 'orderDate', 'tid'] _valid_product_fields = ['id', 'sku', 'name', 'description', 'category', @@ -33,6 +46,9 @@ class Client(object): _cached_messages = {} _cached_all_messages = False + _cached_workflows = {} + _cached_all_workflows = False + def __init__(self, token, **kwargs): if not token or not isinstance(token, six.string_types): raise ValueError('Must supply a token as a non empty string.') @@ -40,15 +56,24 @@ def __init__(self, token, **kwargs): self._token = token self._client = None + def update_contact_fields(self, merge_vars): + if isinstance(merge_vars, dict): + temp = self._valid_contact_fields + for var in merge_vars.keys(): + if var not in temp: + temp.append(var) + self._valid_contact_fields = temp + def login(self): self._client = suds.client.Client(API_ENDPOINT) + self._client.set_options(timeout=25) try: self.session_id = self._client.service.login(self._token) session_header = self._client.factory.create('sessionHeader') session_header.sessionId = self.session_id self._client.set_options(soapheaders=session_header) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) def _construct_contact_fields(self, fields): final_fields = [] @@ -58,15 +83,15 @@ def _construct_contact_fields(self, fields): real_field = list(filter(lambda x: x.name == field_key, real_fields))[0] except IndexError: - raise BrontoError('Invalid contactField: %s' % - field_key) + raise BrontoException('Invalid contactField: %s' % + field_key) field_object = self._client.factory.create('contactField') field_object.fieldId = real_field.id field_object.content = field_val final_fields.append(field_object) return final_fields - def add_contacts(self, contacts): + def add_contacts(self, contacts, list_id=None): final_contacts = [] for contact in contacts: if not any([contact.get('email'), contact.get('mobileNumber')]): @@ -81,6 +106,8 @@ def add_contacts(self, contacts): raise KeyError('Invalid contact attribute: %s' % field) else: setattr(contact_obj, field, value) + contact_obj.listIds = list_id + contact_obj.status = 'onboarding' final_contacts.append(contact_obj) try: response = self._client.service.addContacts(final_contacts) @@ -88,19 +115,46 @@ def add_contacts(self, contacts): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError('An error occurred while adding contacts: %s' - % err_str) + err_code = response.results[0].errorCode + raise BrontoException('An error occurred while adding contacts: %s' + % err_str, err_code) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response - def add_contact(self, contact): - contact = self.add_contacts([contact, ]) + def add_contact(self, contact, list_id=None): + contact = self.add_contacts([contact, ], list_id) try: return contact.results[0] except: return contact.results + def get_contacts_by_list(self, list_id, status_filter=None, fields=None): + contact_filter = self._client.factory.create('contactFilter') + contact_filter.listId = list_id + if status_filter: + contact_filter.status = [status_val for status_val in status_filter] + contact_filter.type = self._client.factory.create('filterType').OR + try: + page_number = 1 + process = True + all_contacts = [] + while process: + response = self._client.service.readContacts( + contact_filter, + includeLists=True, + pageNumber=page_number, + fields=fields, + ) + if len(response): + all_contacts += response + page_number += 1 + else: + process = False + except WebFault as e: + raise BrontoException(e.message) + return all_contacts + def get_contacts(self, emails, include_lists=False, fields=[], page_number=1, include_sms=False): final_emails = [] @@ -129,7 +183,7 @@ def get_contacts(self, emails, include_lists=False, fields=[], pageNumber=page_number, includeSMSKeywords=include_sms) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def get_contact(self, email, include_lists=False, fields=[], @@ -141,7 +195,7 @@ def get_contact(self, email, include_lists=False, fields=[], except: return contact - def update_contacts(self, contacts): + def update_contacts(self, contacts, status=None): """ >>> client.update_contacts({'me@domain.com': {'mobileNumber': '1234567890', @@ -163,7 +217,7 @@ def update_contacts(self, contacts): real_contact = list(filter(lambda x: x.email == email, contact_objs))[0] except IndexError: - raise BrontoError('Contact not found: %s' % email) + raise BrontoException('Contact not found: %s' % email) for field, value in six.iteritems(contact_info): if field == 'fields': field_objs = self._construct_contact_fields(value) @@ -180,6 +234,8 @@ def update_contacts(self, contacts): raise KeyError('Invalid contact attribute: %s' % field) else: setattr(real_contact, field, value) + if status: + real_contact.status = status final_contacts.append(real_contact) try: response = self._client.service.updateContacts(final_contacts) @@ -187,13 +243,13 @@ def update_contacts(self, contacts): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError('An error occurred while adding contacts: %s' - % err_str) + raise BrontoException('An error occurred while adding contacts: %s' + % err_str) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response - def update_contact(self, email, contact_info): + def update_contact(self, email, contact_info, status): contact = self.update_contacts({email: contact_info}) try: return contact.results[0] @@ -224,12 +280,52 @@ def add_or_update_contacts(self, contacts): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError('An error occurred while adding contacts: %s' - % err_str) + raise BrontoException('An error occurred while adding contacts: %s' + % err_str) + except WebFault as e: + raise BrontoException(e.message) + return response + + def add_or_update_contacts_incremental(self, contacts, list_id=None, status=None): + final_contacts = [] + for contact in contacts: + if not any([contact.get('id'), contact.get('email'), + contact.get('mobileNumber')]): + raise ValueError('Must provide one of: id, email, mobileNumber') + contact_obj = self._client.factory.create('contactObject') + for field, value in six.iteritems(contact): + if field == 'fields': + field_objs = self._construct_contact_fields(value) + contact_obj.fields = field_objs + elif field not in self._valid_contact_fields: + raise KeyError('Invalid contact attribute: %s' % field) + else: + setattr(contact_obj, field, value) + if list_id: + contact_obj.listIds = list_id + if status: + contact_obj.status = status + final_contacts.append(contact_obj) + try: + response = self._client.service.addOrUpdateContactsIncremental(final_contacts) + if hasattr(response, 'errors'): + err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, + response.results[x].errorString) + for x in response.errors]) + raise BrontoException('An error occurred while adding contacts: %s' + % err_str) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response + def add_or_update_contact_incremental(self, contact, list_id=None, status=None): + contact = self.add_or_update_contacts_incremental([contact, ], list_id, status) + try: + return contact.results[0] + except: + return contact.results + + def add_or_update_contact(self, contact): contact = self.add_or_update_contacts([contact, ]) try: @@ -242,7 +338,7 @@ def delete_contacts(self, emails): try: response = self._client.service.deleteContacts(contacts) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def delete_contact(self, email): @@ -281,10 +377,10 @@ def add_orders(self, orders): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError('An error occurred while adding orders: %s' - % err_str) + raise BrontoException('An error occurred while adding orders: %s' + % err_str) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def add_order(self, order): @@ -313,7 +409,7 @@ def delete_orders(self, order_ids): try: response = self._client.service.deleteOrders(orders) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def delete_order(self, order_id): @@ -361,12 +457,12 @@ def add_fields(self, fields): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError('An error occurred while adding fields: %s' - % err_str) + raise BrontoException('An error occurred while adding fields: %s' + % err_str) # If no error we force to refresh the fields' cache self._cached_all_fields = False except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def add_field(self, field): @@ -407,7 +503,7 @@ def get_fields(self, field_names=[]): if not len(final_fields): self._cached_all_fields = True except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) else: if not field_names: response = [y for x, y in six.iteritems(self._cached_fields)] @@ -431,7 +527,7 @@ def delete_fields(self, field_ids): try: response = self._client.service.deleteFields(fields) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def delete_field(self, field_id): @@ -471,12 +567,12 @@ def add_lists(self, lists): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError('An error occurred while adding fields: %s' - % err_str) + raise BrontoException('An error occurred while adding fields: %s' + % err_str) # If no error we force to refresh the fields' cache self._cached_all_fields = False except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def add_list(self, list_): @@ -521,7 +617,7 @@ def get_lists(self, list_names=[]): if not len(final_lists): self._cached_all_lists = True except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) else: if not list_names: response = [y for x, y in six.iteritems(self._cached_lists)] @@ -545,7 +641,7 @@ def delete_lists(self, list_ids): try: response = self._client.service.deleteLists(lists) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def delete_list(self, list_id): @@ -595,13 +691,13 @@ def add_contacts_to_list(self, list_, contacts): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError( + raise BrontoException( 'An error occurred while adding contacts to a list: %s' % err_str) # If no error we force to refresh the fields' cache self._cached_all_fields = False except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def add_contact_to_list(self, list_, contact): @@ -615,7 +711,7 @@ def add_contact_to_list(self, list_, contact): except: return request.results - def get_messages(self, message_names=[]): + def get_messages(self, message_names=[], include_transactional=False): #TODO: Support search per message_id final_messages = [] cached = [] @@ -641,13 +737,14 @@ def get_messages(self, message_names=[]): if not self._cached_all_messages: try: response = self._client.service.readMessages(message_filter, - pageNumber=1) + pageNumber=1, + includeTransactionalApproval=include_transactional) for message in response: self._cached_messages[message.name] = message if not len(final_messages): self._cached_all_messages = True except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) else: if not message_names: response = [y for x, y in six.iteritems(self._cached_messages)] @@ -715,10 +812,10 @@ def add_deliveries(self, deliveries): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) - raise BrontoError('An error occurred while adding deliveries: %s' - % err_str) + raise BrontoException('An error occurred while adding deliveries: %s' + % err_str) except WebFault as e: - raise BrontoError(e.message) + raise BrontoException(e.message) return response def add_delivery(self, delivery): @@ -727,3 +824,97 @@ def add_delivery(self, delivery): return request.results[0] except: return request.results + + def get_workflows(self, workflows=[]): + """ + + """ + final_workflows = [] + cached = [] + filter_operator = self._client.factory.create('filterOperator') + fop = filter_operator.EqualTo + for workflow in workflows: + if workflow in self._cached_workflows: + cached.append(self._cached_workflows[workflow]) + else: + workflow_string = self._client.factory.create('stringValue') + workflow_string.operator = fop + workflow_string.value = workflow + final_workflows.append(workflow_string) + if workflows and not final_workflows: + return [self._cached_workflows.get(workflow) for workflow in workflows] + + + workflow_filter = self._client.factory.create('workflowFilter') + workflow_filter.name = final_workflows + filter_type = self._client.factory.create('filterType') + workflow_filter.type = filter_type.OR + + if not self._cached_all_workflows: + try: + response = self._client.service.readWorkflows(workflow_filter, + pageNumber=1) + for workflow in response: + self._cached_workflows[workflow.name] = workflow + if not len(final_workflows): + self._cached_all_workflows = True + except WebFault as e: + raise BrontoException(e.message) + else: + if not workflows: + response = [y for x, y in six.iteritems(self._cached_workflows)] + else: + response = [] + return response + cached + def get_workflow(self, workflow): + """ + workflow: {'id':'myworkflowid','name':'name'} can search by name or by ID + """ + request = self.get_workflows([workflow, ]) + try: + return request[0] + except: + return request.results + def get_workflow_by_id(self, id): + request = self.get_workflows_by_id([id]) + try: + return request[0] + except: + return [] + def get_workflows_by_id(self, ids): + """ + + """ + workflow_filter = self._client.factory.create('workflowFilter') + workflow_filter.id = ids + filter_type = self._client.factory.create('filterType') + workflow_filter.type = filter_type.OR + workflow_results = [] + response = [] + if not self._cached_all_workflows: + workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if getattr(workflow, 'id', None) in ids] + if len(ids) == len(workflow_results): + return workflow_results + try: + response = self._client.service.readWorkflows(filter=workflow_filter, pageNumber=1) + for workflow in response: + self._cached_workflows[workflow.name] = workflow + except WebFault as e: + raise BrontoException(e.message) + else: + workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if getattr(workflow, 'id', None) in ids] + return workflow_results + response + + + # return [self._cached_workflows.get(workflow.name) for workflow in self._cached_workflows.items() if workflow.id in ids] + def add_contacts_to_workflow(self, email, workflow_id): + workflow = self._client.factory.create('workflowObject') + contact = self._client.factory.create('contactObject') + workflow.id = workflow_id + contact.email = email + try: + response = self._client.service.addContactsToWorkflow(workflow, contact) + except WebFault as e: + raise BrontoException(e.message) + return response + From 5bc1b96acc54277717e31dc73c6ba8ec7f8335f3 Mon Sep 17 00:00:00 2001 From: Josh Date: Thu, 2 Jan 2020 16:13:10 +0000 Subject: [PATCH 2/3] Case 8603 - Moving changes into bronto-python repo --- bronto/client.py | 67 ++++++++++++++++++++++++------------------------ setup.py | 6 ++--- 2 files changed, 36 insertions(+), 37 deletions(-) diff --git a/bronto/client.py b/bronto/client.py index 590442c..67dd70b 100644 --- a/bronto/client.py +++ b/bronto/client.py @@ -6,6 +6,7 @@ API_ENDPOINT = 'https://api.bronto.com/v4?wsdl' + class BrontoException(Exception): def __init__(self, message, code=None): # try extract the status code with regex @@ -19,12 +20,10 @@ def __init__(self, message, code=None): self.code = code - - class Client(object): _valid_contact_fields = ['email', 'mobileNumber', 'status', 'msgPref', 'source', 'customSource', 'listIds', 'fields', - 'SMSKeywordIDs',] #'first_name', 'last_name', 'cart_html'] + 'SMSKeywordIDs', ] # 'first_name', 'last_name', 'cart_html'] _valid_order_fields = ['id', 'email', 'contactId', 'products', 'orderDate', 'tid'] @@ -33,9 +32,9 @@ class Client(object): _valid_field_fields = ['id', 'name', 'label', 'type', 'visibility', 'options'] _valid_list_fields = ['id', 'name', 'label'] _valid_delivery_fields = ['authentication', 'fatigueOverride', 'fields', - 'fromEmail', 'fromName', 'messageId', 'messageRuleId', 'optin', - 'recipients', 'remail', 'replyEmail', 'replyTracking', 'start', - 'throttle', 'type'] + 'fromEmail', 'fromName', 'messageId', 'messageRuleId', 'optin', + 'recipients', 'remail', 'replyEmail', 'replyTracking', 'start', + 'throttle', 'type'] _cached_fields = {} _cached_all_fields = False @@ -97,7 +96,6 @@ def add_contacts(self, contacts, list_id=None): if not any([contact.get('email'), contact.get('mobileNumber')]): raise ValueError('Must provide either an email or mobileNumber') contact_obj = self._client.factory.create('contactObject') - # FIXME: Add special handling for listIds, SMSKeywordIDs for field, value in six.iteritems(contact): if field == 'fields': field_objs = self._construct_contact_fields(value) @@ -177,11 +175,11 @@ def get_contacts(self, emails, include_lists=False, fields=[], field_objs = self.get_fields(fields) field_ids = [x.id for x in field_objs] response = self._client.service.readContacts( - contact_filter, - includeLists=include_lists, - fields=field_ids, - pageNumber=page_number, - includeSMSKeywords=include_sms) + contact_filter, + includeLists=include_lists, + fields=field_ids, + pageNumber=page_number, + includeSMSKeywords=include_sms) except WebFault as e: raise BrontoException(e.message) return response @@ -325,7 +323,6 @@ def add_or_update_contact_incremental(self, contact, list_id=None, status=None): except: return contact.results - def add_or_update_contact(self, contact): contact = self.add_or_update_contacts([contact, ]) try: @@ -443,7 +440,7 @@ def add_fields(self, fields): for field in fields: if not all(key in field for key in required_attributes): raise ValueError('The attributes %s are required.' - % required_attributes) + % required_attributes) field_obj = self._client.factory.create('fieldObject') for attribute, value in six.iteritems(field): if attribute not in self._valid_field_fields: @@ -473,7 +470,7 @@ def add_field(self, field): return request.results def get_fields(self, field_names=[]): - #TODO: Support search per field_id + # TODO: Support search per field_id final_fields = [] cached = [] filter_operator = self._client.factory.create('filterOperator') @@ -550,7 +547,7 @@ def add_lists(self, lists): """ required_attributes = ['name', 'label'] final_lists = [] - for list_ in lists: # Use list_ as list is a built-in object + for list_ in lists: # Use list_ as list is a built-in object if not all(key in list_ for key in required_attributes): raise ValueError('The attributes %s are required.' % required_attributes) @@ -583,7 +580,7 @@ def add_list(self, list_): return request.results def get_lists(self, list_names=[]): - #TODO: Support search per list_id + # TODO: Support search per list_id final_lists = [] cached = [] filter_operator = self._client.factory.create('filterOperator') @@ -597,7 +594,7 @@ def get_lists(self, list_names=[]): list_string.value = list_name final_lists.append(list_string) - if list_names and not final_lists: # if we already have all the lists + if list_names and not final_lists: # if we already have all the lists return [self._cached_lists.get(name) for name in list_names] list_filter = self._client.factory.create('mailListFilter') @@ -651,7 +648,6 @@ def delete_list(self, list_id): except: return response.results - def add_contacts_to_list(self, list_, contacts): """ The list must have either an id or a name defined. @@ -667,7 +663,7 @@ def add_contacts_to_list(self, list_, contacts): if not any(key in list_ for key in valid_list_attributes): raise ValueError('Must provide either a name ' - 'or id for your lists.') + 'or id for your lists.') final_list = self._client.factory.create('mailListObject') for attribute in valid_list_attributes: if attribute in list_: @@ -677,7 +673,7 @@ def add_contacts_to_list(self, list_, contacts): for contact in contacts: if not any(key in contact for key in valid_contact_attributes): raise ValueError('Must provide either an email ' - 'or id for your contacts.') + 'or id for your contacts.') contact_obj = self._client.factory.create('contactObject') for attribute in valid_contact_attributes: if attribute in contact: @@ -685,15 +681,15 @@ def add_contacts_to_list(self, list_, contacts): final_contacts.append(contact_obj) try: response = self._client.service.addToList(final_list, - final_contacts) + final_contacts) if hasattr(response, 'errors'): err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, response.results[x].errorString) for x in response.errors]) raise BrontoException( - 'An error occurred while adding contacts to a list: %s' - % err_str) + 'An error occurred while adding contacts to a list: %s' + % err_str) # If no error we force to refresh the fields' cache self._cached_all_fields = False except WebFault as e: @@ -705,14 +701,14 @@ def add_contact_to_list(self, list_, contact): Add one contact to one list """ - request = self.add_contacts_to_list(list_, [contact,]) + request = self.add_contacts_to_list(list_, [contact, ]) try: return request.results[0] except: return request.results def get_messages(self, message_names=[], include_transactional=False): - #TODO: Support search per message_id + # TODO: Support search per message_id final_messages = [] cached = [] filter_operator = self._client.factory.create('filterOperator') @@ -726,7 +722,7 @@ def get_messages(self, message_names=[], include_transactional=False): message_string.value = message_name final_messages.append(message_string) - if message_names and not final_messages: # if we already have all the messages + if message_names and not final_messages: # if we already have all the messages return [self._cached_messages.get(name) for name in message_names] message_filter = self._client.factory.create('messageFilter') @@ -792,8 +788,8 @@ def add_deliveries(self, deliveries): For more details: http://dev.bronto.com/api/v4/data-format """ required_attributes = ['start', 'messageId', 'type', 'fromEmail', - #'replyEmail', Even if the doc says so, it's not required - 'fromName', 'recipients'] + # 'replyEmail', Even if the doc says so, it's not required + 'fromName', 'recipients'] final_deliveries = [] for delivery in deliveries: if not all(key in delivery for key in required_attributes): @@ -844,7 +840,6 @@ def get_workflows(self, workflows=[]): if workflows and not final_workflows: return [self._cached_workflows.get(workflow) for workflow in workflows] - workflow_filter = self._client.factory.create('workflowFilter') workflow_filter.name = final_workflows filter_type = self._client.factory.create('filterType') @@ -866,6 +861,7 @@ def get_workflows(self, workflows=[]): else: response = [] return response + cached + def get_workflow(self, workflow): """ workflow: {'id':'myworkflowid','name':'name'} can search by name or by ID @@ -875,12 +871,14 @@ def get_workflow(self, workflow): return request[0] except: return request.results + def get_workflow_by_id(self, id): request = self.get_workflows_by_id([id]) try: return request[0] except: return [] + def get_workflows_by_id(self, ids): """ @@ -892,7 +890,8 @@ def get_workflows_by_id(self, ids): workflow_results = [] response = [] if not self._cached_all_workflows: - workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if getattr(workflow, 'id', None) in ids] + workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if + getattr(workflow, 'id', None) in ids] if len(ids) == len(workflow_results): return workflow_results try: @@ -902,11 +901,12 @@ def get_workflows_by_id(self, ids): except WebFault as e: raise BrontoException(e.message) else: - workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if getattr(workflow, 'id', None) in ids] + workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if + getattr(workflow, 'id', None) in ids] return workflow_results + response + # return [self._cached_workflows.get(workflow.name) for workflow in self._cached_workflows.items() if workflow.id in ids] - # return [self._cached_workflows.get(workflow.name) for workflow in self._cached_workflows.items() if workflow.id in ids] def add_contacts_to_workflow(self, email, workflow_id): workflow = self._client.factory.create('workflowObject') contact = self._client.factory.create('contactObject') @@ -917,4 +917,3 @@ def add_contacts_to_workflow(self, email, workflow_id): except WebFault as e: raise BrontoException(e.message) return response - diff --git a/setup.py b/setup.py index 180c0a9..5b51701 100644 --- a/setup.py +++ b/setup.py @@ -7,13 +7,13 @@ os.chdir(os.path.normpath(os.path.join(os.path.abspath(__file__), os.pardir))) VERSION = bronto.__version__ -github_url = 'http://github.com/Scotts-Marketplace/bronto-python/' +github_url = 'https://github.com/TriggeredMessaging/bronto-python.git' requires = ['suds-jurko', 'six'] setup(name='bronto-python', version=VERSION, - author='Joey Wilhelm', - author_email='joey@scottsmarketplace.com', + author='Fresh Relevance', + author_email='hello@freshrelevance.com', license='Apache', url=github_url, download_url='%sarchive/%s.tar.gz' % (github_url, VERSION), From d90cc7c3cd152468dff6b5f107ea8f4a51218290 Mon Sep 17 00:00:00 2001 From: Josh Date: Fri, 3 Jan 2020 15:47:12 +0000 Subject: [PATCH 3/3] Case 8603 - WIP --- .gitignore | 37 -- .travis.yml | 12 - LICENSE | 202 ---------- MANIFEST.in | 5 - README.rst | 150 -------- bronto/__init__.py | 1 - bronto/client.py | 919 --------------------------------------------- requirements.txt | 1 - 8 files changed, 1327 deletions(-) delete mode 100644 .gitignore delete mode 100644 .travis.yml delete mode 100644 LICENSE delete mode 100644 MANIFEST.in delete mode 100644 README.rst delete mode 100644 bronto/__init__.py delete mode 100644 bronto/client.py delete mode 100644 requirements.txt diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 0c18eef..0000000 --- a/.gitignore +++ /dev/null @@ -1,37 +0,0 @@ -*.py[cod] - -# C extensions -*.so - -# Packages -*.egg -*.egg-info -dist -build -eggs -parts -bin -var -sdist -develop-eggs -.installed.cfg -lib -lib64 -__pycache__ - -# Installer logs -pip-log.txt - -# Unit test / coverage reports -.coverage -htmlcov -.tox -nosetests.xml - -# Translations -*.mo - -# Mr Developer -.mr.developer.cfg -.project -.pydevproject diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ae26af8..0000000 --- a/.travis.yml +++ /dev/null @@ -1,12 +0,0 @@ -language: python -python: - - "2.7" -install: - - pip install -r requirements.txt - - pip install coveralls -env: - - secure: "SuaLT9BPAeq29+0LJzZqUfYA5dKWPVLxKHC9CdtVW8dQArcCMQNuB5yKNbxhcxbu+mdjNtUNPygFi2VFTZyg2wjzAUEmdnyXX+r3lmv9y108gWveq5ox2v6cRdXapsBn+O/yCF05MjzzlUTxNwRb8z8ERuUFVNETCDzSwVTmZew=" -script: - - coverage run --source=bronto test.py -after_success: - coveralls diff --git a/LICENSE b/LICENSE deleted file mode 100644 index e06d208..0000000 --- a/LICENSE +++ /dev/null @@ -1,202 +0,0 @@ -Apache License - Version 2.0, January 2004 - http://www.apache.org/licenses/ - - TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - - 1. Definitions. - - "License" shall mean the terms and conditions for use, reproduction, - and distribution as defined by Sections 1 through 9 of this document. - - "Licensor" shall mean the copyright owner or entity authorized by - the copyright owner that is granting the License. - - "Legal Entity" shall mean the union of the acting entity and all - other entities that control, are controlled by, or are under common - control with that entity. For the purposes of this definition, - "control" means (i) the power, direct or indirect, to cause the - direction or management of such entity, whether by contract or - otherwise, or (ii) ownership of fifty percent (50%) or more of the - outstanding shares, or (iii) beneficial ownership of such entity. - - "You" (or "Your") shall mean an individual or Legal Entity - exercising permissions granted by this License. - - "Source" form shall mean the preferred form for making modifications, - including but not limited to software source code, documentation - source, and configuration files. - - "Object" form shall mean any form resulting from mechanical - transformation or translation of a Source form, including but - not limited to compiled object code, generated documentation, - and conversions to other media types. - - "Work" shall mean the work of authorship, whether in Source or - Object form, made available under the License, as indicated by a - copyright notice that is included in or attached to the work - (an example is provided in the Appendix below). - - "Derivative Works" shall mean any work, whether in Source or Object - form, that is based on (or derived from) the Work and for which the - editorial revisions, annotations, elaborations, or other modifications - represent, as a whole, an original work of authorship. For the purposes - of this License, Derivative Works shall not include works that remain - separable from, or merely link (or bind by name) to the interfaces of, - the Work and Derivative Works thereof. - - "Contribution" shall mean any work of authorship, including - the original version of the Work and any modifications or additions - to that Work or Derivative Works thereof, that is intentionally - submitted to Licensor for inclusion in the Work by the copyright owner - or by an individual or Legal Entity authorized to submit on behalf of - the copyright owner. For the purposes of this definition, "submitted" - means any form of electronic, verbal, or written communication sent - to the Licensor or its representatives, including but not limited to - communication on electronic mailing lists, source code control systems, - and issue tracking systems that are managed by, or on behalf of, the - Licensor for the purpose of discussing and improving the Work, but - excluding communication that is conspicuously marked or otherwise - designated in writing by the copyright owner as "Not a Contribution." - - "Contributor" shall mean Licensor and any individual or Legal Entity - on behalf of whom a Contribution has been received by Licensor and - subsequently incorporated within the Work. - - 2. Grant of Copyright License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - copyright license to reproduce, prepare Derivative Works of, - publicly display, publicly perform, sublicense, and distribute the - Work and such Derivative Works in Source or Object form. - - 3. Grant of Patent License. Subject to the terms and conditions of - this License, each Contributor hereby grants to You a perpetual, - worldwide, non-exclusive, no-charge, royalty-free, irrevocable - (except as stated in this section) patent license to make, have made, - use, offer to sell, sell, import, and otherwise transfer the Work, - where such license applies only to those patent claims licensable - by such Contributor that are necessarily infringed by their - Contribution(s) alone or by combination of their Contribution(s) - with the Work to which such Contribution(s) was submitted. If You - institute patent litigation against any entity (including a - cross-claim or counterclaim in a lawsuit) alleging that the Work - or a Contribution incorporated within the Work constitutes direct - or contributory patent infringement, then any patent licenses - granted to You under this License for that Work shall terminate - as of the date such litigation is filed. - - 4. Redistribution. You may reproduce and distribute copies of the - Work or Derivative Works thereof in any medium, with or without - modifications, and in Source or Object form, provided that You - meet the following conditions: - - (a) You must give any other recipients of the Work or - Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices - stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works - that You distribute, all copyright, patent, trademark, and - attribution notices from the Source form of the Work, - excluding those notices that do not pertain to any part of - the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its - distribution, then any Derivative Works that You distribute must - include a readable copy of the attribution notices contained - within such NOTICE file, excluding those notices that do not - pertain to any part of the Derivative Works, in at least one - of the following places: within a NOTICE text file distributed - as part of the Derivative Works; within the Source form or - documentation, if provided along with the Derivative Works; or, - within a display generated by the Derivative Works, if and - wherever such third-party notices normally appear. The contents - of the NOTICE file are for informational purposes only and - do not modify the License. You may add Your own attribution - notices within Derivative Works that You distribute, alongside - or as an addendum to the NOTICE text from the Work, provided - that such additional attribution notices cannot be construed - as modifying the License. - - You may add Your own copyright statement to Your modifications and - may provide additional or different license terms and conditions - for use, reproduction, or distribution of Your modifications, or - for any such Derivative Works as a whole, provided Your use, - reproduction, and distribution of the Work otherwise complies with - the conditions stated in this License. - - 5. Submission of Contributions. Unless You explicitly state otherwise, - any Contribution intentionally submitted for inclusion in the Work - by You to the Licensor shall be under the terms and conditions of - this License, without any additional terms or conditions. - Notwithstanding the above, nothing herein shall supersede or modify - the terms of any separate license agreement you may have executed - with Licensor regarding such Contributions. - - 6. Trademarks. This License does not grant permission to use the trade - names, trademarks, service marks, or product names of the Licensor, - except as required for reasonable and customary use in describing the - origin of the Work and reproducing the content of the NOTICE file. - - 7. Disclaimer of Warranty. Unless required by applicable law or - agreed to in writing, Licensor provides the Work (and each - Contributor provides its Contributions) on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or - implied, including, without limitation, any warranties or conditions - of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A - PARTICULAR PURPOSE. You are solely responsible for determining the - appropriateness of using or redistributing the Work and assume any - risks associated with Your exercise of permissions under this License. - - 8. Limitation of Liability. In no event and under no legal theory, - whether in tort (including negligence), contract, or otherwise, - unless required by applicable law (such as deliberate and grossly - negligent acts) or agreed to in writing, shall any Contributor be - liable to You for damages, including any direct, indirect, special, - incidental, or consequential damages of any character arising as a - result of this License or out of the use or inability to use the - Work (including but not limited to damages for loss of goodwill, - work stoppage, computer failure or malfunction, or any and all - other commercial damages or losses), even if such Contributor - has been advised of the possibility of such damages. - - 9. Accepting Warranty or Additional Liability. While redistributing - the Work or Derivative Works thereof, You may choose to offer, - and charge a fee for, acceptance of support, warranty, indemnity, - or other liability obligations and/or rights consistent with this - License. However, in accepting such obligations, You may act only - on Your own behalf and on Your sole responsibility, not on behalf - of any other Contributor, and only if You agree to indemnify, - defend, and hold each Contributor harmless for any liability - incurred by, or claims asserted against, such Contributor by reason - of your accepting any such warranty or additional liability. - - END OF TERMS AND CONDITIONS - - APPENDIX: How to apply the Apache License to your work. - - To apply the Apache License to your work, attach the following - boilerplate notice, with the fields enclosed by brackets "{}" - replaced with your own identifying information. (Don't include - the brackets!) The text should be enclosed in the appropriate - comment syntax for the file format. We also recommend that a - file or class name and description of purpose be included on the - same "printed page" as the copyright notice for easier - identification within third-party archives. - - Copyright {yyyy} {name of copyright owner} - - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. - diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 0640fe9..0000000 --- a/MANIFEST.in +++ /dev/null @@ -1,5 +0,0 @@ -include AUTHORS -include CHANGELOG -include LICENSE -include README.rst -prune build diff --git a/README.rst b/README.rst deleted file mode 100644 index 14e6105..0000000 --- a/README.rst +++ /dev/null @@ -1,150 +0,0 @@ -bronto-python -============= - -bronto-python is a python query client which wraps the Bronto SOAP API in an -easy to use manner, using the `suds `_ library. - -.. image:: https://travis-ci.org/Scotts-Marketplace/bronto-python.svg?branch=master - :target: https://travis-ci.org/Scotts-Marketplace/bronto-python - -.. image:: https://coveralls.io/repos/Scotts-Marketplace/bronto-python/badge.png?branch=master - :target: https://coveralls.io/r/Scotts-Marketplace/bronto-python?branch=master - -.. image:: https://pypip.in/d/bronto-python/badge.png - :target: https://crate.io/packages/bronto-python/ - -Getting Started -=============== - -.. code:: python - - from bronto.client import Client - - client = Client('BRONTO_API_TOKEN') - client.login() - -Simple as that! - -Contacts -======== - -Adding a Contact ----------------- - -.. code:: python - - contact_data = {'email': 'me@domain.com', - 'source': 'api', - 'customSource': 'Using bronto-python to import my contact'} - client.add_contact(contact_data) - -Retrieving a contact --------------------- - -.. code:: python - - client.get_contact('me@domain.com') - -Deleting a contact ------------------- - -.. code:: python - - client.delete_contact('me@domain.com') - -Orders -====== - -Adding an order ---------------- - -.. code:: python - - order_data = {'id': 'xyz123', - 'email': 'me@domain.com', - 'products': [ - {'id': 1, - 'sku': '1111', - 'name': 'Test Product 1', - 'description': 'This is our first test product.', - 'quantity': 1, - 'price': 3.50}, - {'id': 2, - 'sku': '2222', - 'name': 'Second Test Product', - 'description': 'Here we have another product for testing.', - 'quantity': 12, - 'price': 42.00} - ] - } - client.add_order(order_data) - -Deleting an order ------------------ - -.. code:: python - - client.delete_order('xyz123') # Orders are deleted by their orderId - -FIELDS -====== - -Adding a field --------------- - -.. code:: python - - field_data = {'name': 'my_field', - 'label': 'My Field', - 'type': 'text', - 'visible': 'private' - } - client.add_field(field_data) - -Retrieving a field ------------------- - -.. code:: python - - client.get_field('my_field') - -Deleting a field ----------------- - -.. code:: python - - field = client.get_field('my_field') - client.delete_field(field.id) - -LISTS -===== - -Adding a list -------------- - -.. code:: python - - list_data = {'name': 'my_list', - 'label': 'My List' - } - client.add_list(list_data) - -Retrieving a list ------------------ - -.. code:: python - - client.get_list('my_list') - -Deleting a list ---------------- - -.. code:: python - - list_to_del = client.get_list('my_list') - client.delete_field(list_to_del.id) - - -**NOTE:** This client is not built with long-running processes in mind. The -Bronto API connection will time out after 20 minutes of inactivity, and this -client does NOT handle those timeouts. diff --git a/bronto/__init__.py b/bronto/__init__.py deleted file mode 100644 index 32a90a3..0000000 --- a/bronto/__init__.py +++ /dev/null @@ -1 +0,0 @@ -__version__ = '0.8.0' diff --git a/bronto/client.py b/bronto/client.py deleted file mode 100644 index 67dd70b..0000000 --- a/bronto/client.py +++ /dev/null @@ -1,919 +0,0 @@ -import re - -import six -import suds.client -from suds import WebFault - -API_ENDPOINT = 'https://api.bronto.com/v4?wsdl' - - -class BrontoException(Exception): - def __init__(self, message, code=None): - # try extract the status code with regex - if not code: - code = re.search('\d{3}', message) - try: - code = int(message[code.regs[0][0]:code.regs[0][1]]) - except Exception as e: - code = None - self.message = message - self.code = code - - -class Client(object): - _valid_contact_fields = ['email', 'mobileNumber', 'status', 'msgPref', - 'source', 'customSource', 'listIds', 'fields', - 'SMSKeywordIDs', ] # 'first_name', 'last_name', 'cart_html'] - - _valid_order_fields = ['id', 'email', 'contactId', 'products', 'orderDate', - 'tid'] - _valid_product_fields = ['id', 'sku', 'name', 'description', 'category', - 'image', 'url', 'quantity', 'price'] - _valid_field_fields = ['id', 'name', 'label', 'type', 'visibility', 'options'] - _valid_list_fields = ['id', 'name', 'label'] - _valid_delivery_fields = ['authentication', 'fatigueOverride', 'fields', - 'fromEmail', 'fromName', 'messageId', 'messageRuleId', 'optin', - 'recipients', 'remail', 'replyEmail', 'replyTracking', 'start', - 'throttle', 'type'] - - _cached_fields = {} - _cached_all_fields = False - - _cached_lists = {} - _cached_all_lists = False - - _cached_messages = {} - _cached_all_messages = False - - _cached_workflows = {} - _cached_all_workflows = False - - def __init__(self, token, **kwargs): - if not token or not isinstance(token, six.string_types): - raise ValueError('Must supply a token as a non empty string.') - - self._token = token - self._client = None - - def update_contact_fields(self, merge_vars): - if isinstance(merge_vars, dict): - temp = self._valid_contact_fields - for var in merge_vars.keys(): - if var not in temp: - temp.append(var) - self._valid_contact_fields = temp - - def login(self): - self._client = suds.client.Client(API_ENDPOINT) - self._client.set_options(timeout=25) - try: - self.session_id = self._client.service.login(self._token) - session_header = self._client.factory.create('sessionHeader') - session_header.sessionId = self.session_id - self._client.set_options(soapheaders=session_header) - except WebFault as e: - raise BrontoException(e.message) - - def _construct_contact_fields(self, fields): - final_fields = [] - real_fields = self.get_fields(fields.keys()) - for field_key, field_val in six.iteritems(fields): - try: - real_field = list(filter(lambda x: x.name == field_key, - real_fields))[0] - except IndexError: - raise BrontoException('Invalid contactField: %s' % - field_key) - field_object = self._client.factory.create('contactField') - field_object.fieldId = real_field.id - field_object.content = field_val - final_fields.append(field_object) - return final_fields - - def add_contacts(self, contacts, list_id=None): - final_contacts = [] - for contact in contacts: - if not any([contact.get('email'), contact.get('mobileNumber')]): - raise ValueError('Must provide either an email or mobileNumber') - contact_obj = self._client.factory.create('contactObject') - for field, value in six.iteritems(contact): - if field == 'fields': - field_objs = self._construct_contact_fields(value) - contact_obj.fields = field_objs - elif field not in self._valid_contact_fields: - raise KeyError('Invalid contact attribute: %s' % field) - else: - setattr(contact_obj, field, value) - contact_obj.listIds = list_id - contact_obj.status = 'onboarding' - final_contacts.append(contact_obj) - try: - response = self._client.service.addContacts(final_contacts) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - err_code = response.results[0].errorCode - raise BrontoException('An error occurred while adding contacts: %s' - % err_str, err_code) - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_contact(self, contact, list_id=None): - contact = self.add_contacts([contact, ], list_id) - try: - return contact.results[0] - except: - return contact.results - - def get_contacts_by_list(self, list_id, status_filter=None, fields=None): - contact_filter = self._client.factory.create('contactFilter') - contact_filter.listId = list_id - if status_filter: - contact_filter.status = [status_val for status_val in status_filter] - contact_filter.type = self._client.factory.create('filterType').OR - try: - page_number = 1 - process = True - all_contacts = [] - while process: - response = self._client.service.readContacts( - contact_filter, - includeLists=True, - pageNumber=page_number, - fields=fields, - ) - if len(response): - all_contacts += response - page_number += 1 - else: - process = False - except WebFault as e: - raise BrontoException(e.message) - return all_contacts - - def get_contacts(self, emails, include_lists=False, fields=[], - page_number=1, include_sms=False): - final_emails = [] - filter_operator = self._client.factory.create('filterOperator') - fop = filter_operator.EqualTo - for email in emails: - contact_email = self._client.factory.create('stringValue') - contact_email.operator = fop - contact_email.value = email - final_emails.append(contact_email) - contact_filter = self._client.factory.create('contactFilter') - contact_filter.email = final_emails - filter_type = self._client.factory.create('filterType') - if len(final_emails) > 1: - contact_filter.type = filter_type.OR - else: - contact_filter.type = filter_type.AND - - try: - field_objs = self.get_fields(fields) - field_ids = [x.id for x in field_objs] - response = self._client.service.readContacts( - contact_filter, - includeLists=include_lists, - fields=field_ids, - pageNumber=page_number, - includeSMSKeywords=include_sms) - except WebFault as e: - raise BrontoException(e.message) - return response - - def get_contact(self, email, include_lists=False, fields=[], - include_sms=False): - contact = self.get_contacts([email, ], include_lists, fields, 1, - include_sms) - try: - return contact[0] - except: - return contact - - def update_contacts(self, contacts, status=None): - """ - >>> client.update_contacts({'me@domain.com': - {'mobileNumber': '1234567890', - 'fields': - {'firstname': 'New', 'lastname': 'Name'} - }, - 'you@domain.com': - {'email': 'notyou@domain.com', - 'fields': - {'firstname': 'Other', 'lastname': 'Name'} - } - }) - >>> - """ - contact_objs = self.get_contacts(contacts.keys()) - final_contacts = [] - for email, contact_info in six.iteritems(contacts): - try: - real_contact = list(filter(lambda x: x.email == email, - contact_objs))[0] - except IndexError: - raise BrontoException('Contact not found: %s' % email) - for field, value in six.iteritems(contact_info): - if field == 'fields': - field_objs = self._construct_contact_fields(value) - all_fields = self.get_fields() - field_names = dict([(x.id, x.name) for x in all_fields]) - old_fields = dict([(field_names[x.fieldId], x) - for x in real_contact.fields]) - new_fields = dict([(field_names[x.fieldId], x) - for x in field_objs]) - old_fields.update(new_fields) - # This sounds backward, but it's not. Honest. - real_contact.fields = list(old_fields.values()) - elif field not in self._valid_contact_fields: - raise KeyError('Invalid contact attribute: %s' % field) - else: - setattr(real_contact, field, value) - if status: - real_contact.status = status - final_contacts.append(real_contact) - try: - response = self._client.service.updateContacts(final_contacts) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException('An error occurred while adding contacts: %s' - % err_str) - except WebFault as e: - raise BrontoException(e.message) - return response - - def update_contact(self, email, contact_info, status): - contact = self.update_contacts({email: contact_info}) - try: - return contact.results[0] - except: - return contact.results - - def add_or_update_contacts(self, contacts): - # FIXME: This is entirely too similar to add_contacts. - # TODO: These two should be refactored. - final_contacts = [] - for contact in contacts: - if not any([contact.get('id'), contact.get('email'), - contact.get('mobileNumber')]): - raise ValueError('Must provide one of: id, email, mobileNumber') - contact_obj = self._client.factory.create('contactObject') - for field, value in six.iteritems(contact): - if field == 'fields': - field_objs = self._construct_contact_fields(value) - contact_obj.fields = field_objs - elif field not in self._valid_contact_fields: - raise KeyError('Invalid contact attribute: %s' % field) - else: - setattr(contact_obj, field, value) - final_contacts.append(contact_obj) - try: - response = self._client.service.addOrUpdateContacts(final_contacts) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException('An error occurred while adding contacts: %s' - % err_str) - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_or_update_contacts_incremental(self, contacts, list_id=None, status=None): - final_contacts = [] - for contact in contacts: - if not any([contact.get('id'), contact.get('email'), - contact.get('mobileNumber')]): - raise ValueError('Must provide one of: id, email, mobileNumber') - contact_obj = self._client.factory.create('contactObject') - for field, value in six.iteritems(contact): - if field == 'fields': - field_objs = self._construct_contact_fields(value) - contact_obj.fields = field_objs - elif field not in self._valid_contact_fields: - raise KeyError('Invalid contact attribute: %s' % field) - else: - setattr(contact_obj, field, value) - if list_id: - contact_obj.listIds = list_id - if status: - contact_obj.status = status - final_contacts.append(contact_obj) - try: - response = self._client.service.addOrUpdateContactsIncremental(final_contacts) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException('An error occurred while adding contacts: %s' - % err_str) - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_or_update_contact_incremental(self, contact, list_id=None, status=None): - contact = self.add_or_update_contacts_incremental([contact, ], list_id, status) - try: - return contact.results[0] - except: - return contact.results - - def add_or_update_contact(self, contact): - contact = self.add_or_update_contacts([contact, ]) - try: - return contact.results[0] - except: - return contact.results - - def delete_contacts(self, emails): - contacts = self.get_contacts(emails) - try: - response = self._client.service.deleteContacts(contacts) - except WebFault as e: - raise BrontoException(e.message) - return response - - def delete_contact(self, email): - response = self.delete_contacts([email, ]) - try: - return response.results[0] - except: - return response.results - - def add_orders(self, orders): - final_orders = [] - for order in orders: - if not order.get('id', None): - raise ValueError('Each order must provide an id') - order_obj = self._client.factory.create('orderObject') - for field, value in six.iteritems(order): - if field == 'products': - final_products = [] - for product in value: - product_obj = self._client.factory.create('productObject') - for pfield, pvalue in six.iteritems(product): - if pfield not in self._valid_product_fields: - raise KeyError('Invalid product attribute: %s' - % pfield) - setattr(product_obj, pfield, pvalue) - final_products.append(product_obj) - order_obj.products = final_products - elif field not in self._valid_order_fields: - raise KeyError('Invalid order attribute: %s' % field) - else: - setattr(order_obj, field, value) - final_orders.append(order_obj) - try: - response = self._client.service.addOrUpdateOrders(final_orders) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException('An error occurred while adding orders: %s' - % err_str) - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_order(self, order): - order = self.add_orders([order, ]) - try: - return order.results[0] - except: - return order.results - - def get_orders(self, order_ids): - pass - - def get_order(self, order_id): - order = self.get_orders([order_id, ]) - try: - return order[0] - except: - return order - - def delete_orders(self, order_ids): - orders = [] - for order_id in order_ids: - order = self._client.factory.create('orderObject') - order.id = order_id - orders.append(order) - try: - response = self._client.service.deleteOrders(orders) - except WebFault as e: - raise BrontoException(e.message) - return response - - def delete_order(self, order_id): - response = self.delete_orders([order_id, ]) - try: - return response.results[0] - except: - return response.results - - def add_fields(self, fields): - """ - >>> client.add_fields([{ - 'name': 'internal_name', - 'label': 'public_name', - 'type': 'text', - 'visibility': 'private', - }, { - 'name': 'internal_name2', - 'label': 'public_name2', - 'type': 'select', - 'options': [{ - 'value': 'value1', - 'label': 'Value 1', - 'isDefault': 0 - }] - }]) - >>> - """ - required_attributes = ['name', 'label', 'type'] - final_fields = [] - for field in fields: - if not all(key in field for key in required_attributes): - raise ValueError('The attributes %s are required.' - % required_attributes) - field_obj = self._client.factory.create('fieldObject') - for attribute, value in six.iteritems(field): - if attribute not in self._valid_field_fields: - raise KeyError('Invalid field attribute: %s' % attribute) - else: - setattr(field_obj, attribute, value) - final_fields.append(field_obj) - try: - response = self._client.service.addFields(final_fields) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException('An error occurred while adding fields: %s' - % err_str) - # If no error we force to refresh the fields' cache - self._cached_all_fields = False - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_field(self, field): - request = self.add_fields([field, ]) - try: - return request.results[0] - except: - return request.results - - def get_fields(self, field_names=[]): - # TODO: Support search per field_id - final_fields = [] - cached = [] - filter_operator = self._client.factory.create('filterOperator') - fop = filter_operator.EqualTo - for field_name in field_names: - if field_name in self._cached_fields: - cached.append(self._cached_fields[field_name]) - else: - field_string = self._client.factory.create('stringValue') - field_string.operator = fop - field_string.value = field_name - final_fields.append(field_string) - field_filter = self._client.factory.create('fieldsFilter') - field_filter.name = final_fields - filter_type = self._client.factory.create('filterType') - if len(final_fields) > 1: - field_filter.type = filter_type.OR - else: - field_filter.type = filter_type.AND - - if not self._cached_all_fields: - try: - response = self._client.service.readFields(field_filter, - pageNumber=1) - for field in response: - self._cached_fields[field.name] = field - if not len(final_fields): - self._cached_all_fields = True - except WebFault as e: - raise BrontoException(e.message) - else: - if not field_names: - response = [y for x, y in six.iteritems(self._cached_fields)] - else: - response = [] - return response + cached - - def get_field(self, field_name): - field = self.get_fields([field_name, ]) - try: - return field[0] - except: - return field - - def delete_fields(self, field_ids): - fields = [] - for field_id in field_ids: - field = self._client.factory.create('fieldObject') - field.id = field_id - fields.append(field) - try: - response = self._client.service.deleteFields(fields) - except WebFault as e: - raise BrontoException(e.message) - return response - - def delete_field(self, field_id): - response = self.delete_fields([field_id, ]) - try: - return response.results[0] - except: - return response.results - - def add_lists(self, lists): - """ - >>> client.add_lists([{ - 'name': 'internal_name', - 'label': 'Pretty name' - }, { - 'name': 'internal_name2', - 'label': 'Pretty name 2' - }]) - >>> - """ - required_attributes = ['name', 'label'] - final_lists = [] - for list_ in lists: # Use list_ as list is a built-in object - if not all(key in list_ for key in required_attributes): - raise ValueError('The attributes %s are required.' - % required_attributes) - list_obj = self._client.factory.create('mailListObject') - for attribute, value in six.iteritems(list_): - if attribute not in self._valid_list_fields: - raise KeyError('Invalid list attribute: %s' % attribute) - else: - setattr(list_obj, attribute, value) - final_lists.append(list_obj) - try: - response = self._client.service.addLists(final_lists) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException('An error occurred while adding fields: %s' - % err_str) - # If no error we force to refresh the fields' cache - self._cached_all_fields = False - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_list(self, list_): - request = self.add_lists([list_, ]) - try: - return request.results[0] - except: - return request.results - - def get_lists(self, list_names=[]): - # TODO: Support search per list_id - final_lists = [] - cached = [] - filter_operator = self._client.factory.create('filterOperator') - fop = filter_operator.EqualTo - for list_name in list_names: - if list_name in self._cached_lists: - cached.append(self._cached_lists[list_name]) - else: - list_string = self._client.factory.create('stringValue') - list_string.operator = fop - list_string.value = list_name - final_lists.append(list_string) - - if list_names and not final_lists: # if we already have all the lists - return [self._cached_lists.get(name) for name in list_names] - - list_filter = self._client.factory.create('mailListFilter') - list_filter.name = final_lists - filter_type = self._client.factory.create('filterType') - if len(final_lists) > 1: - list_filter.type = filter_type.OR - else: - list_filter.type = filter_type.AND - - if not self._cached_all_lists: - try: - response = self._client.service.readLists(list_filter, - pageNumber=1) - for list_ in response: - self._cached_lists[list_.name] = list_ - if not len(final_lists): - self._cached_all_lists = True - except WebFault as e: - raise BrontoException(e.message) - else: - if not list_names: - response = [y for x, y in six.iteritems(self._cached_lists)] - else: - response = [] - return response + cached - - def get_list(self, list_name): - list_ = self.get_lists([list_name, ]) - try: - return list_[0] - except: - return list_ - - def delete_lists(self, list_ids): - lists = [] - for list_id in list_ids: - list_ = self._client.factory.create('mailListObject') - list_.id = list_id - lists.append(list_) - try: - response = self._client.service.deleteLists(lists) - except WebFault as e: - raise BrontoException(e.message) - return response - - def delete_list(self, list_id): - response = self.delete_lists([list_id, ]) - try: - return response.results[0] - except: - return response.results - - def add_contacts_to_list(self, list_, contacts): - """ - The list must have either an id or a name defined. - The contacts must have either an id or an email defined. - >>> client.add_contacts_to_list({'id': 'xxx-xxx'}, - [{id: 'yyy-yyy'}, {email: 'email2@example.com'}]) - >>> client.add_contacts_to_list({'name': 'my_list'}, - [{id: 'yyy-yyy'}, {email: 'email2@example.com'}]) - >>> - """ - valid_list_attributes = ['id', 'name'] - valid_contact_attributes = ['id', 'email'] - - if not any(key in list_ for key in valid_list_attributes): - raise ValueError('Must provide either a name ' - 'or id for your lists.') - final_list = self._client.factory.create('mailListObject') - for attribute in valid_list_attributes: - if attribute in list_: - setattr(final_list, attribute, list_[attribute]) - - final_contacts = [] - for contact in contacts: - if not any(key in contact for key in valid_contact_attributes): - raise ValueError('Must provide either an email ' - 'or id for your contacts.') - contact_obj = self._client.factory.create('contactObject') - for attribute in valid_contact_attributes: - if attribute in contact: - setattr(contact_obj, attribute, contact[attribute]) - final_contacts.append(contact_obj) - try: - response = self._client.service.addToList(final_list, - final_contacts) - - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException( - 'An error occurred while adding contacts to a list: %s' - % err_str) - # If no error we force to refresh the fields' cache - self._cached_all_fields = False - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_contact_to_list(self, list_, contact): - """ - Add one contact to one list - - """ - request = self.add_contacts_to_list(list_, [contact, ]) - try: - return request.results[0] - except: - return request.results - - def get_messages(self, message_names=[], include_transactional=False): - # TODO: Support search per message_id - final_messages = [] - cached = [] - filter_operator = self._client.factory.create('filterOperator') - fop = filter_operator.EqualTo - for message_name in message_names: - if message_name in self._cached_messages: - cached.append(self._cached_messages[message_name]) - else: - message_string = self._client.factory.create('stringValue') - message_string.operator = fop - message_string.value = message_name - final_messages.append(message_string) - - if message_names and not final_messages: # if we already have all the messages - return [self._cached_messages.get(name) for name in message_names] - - message_filter = self._client.factory.create('messageFilter') - message_filter.name = final_messages - filter_type = self._client.factory.create('filterType') - message_filter.type = filter_type.OR - - if not self._cached_all_messages: - try: - response = self._client.service.readMessages(message_filter, - pageNumber=1, - includeTransactionalApproval=include_transactional) - for message in response: - self._cached_messages[message.name] = message - if not len(final_messages): - self._cached_all_messages = True - except WebFault as e: - raise BrontoException(e.message) - else: - if not message_names: - response = [y for x, y in six.iteritems(self._cached_messages)] - else: - response = [] - return response + cached - - def get_message(self, message_name): - messages = self.get_messages([message_name, ]) - try: - return messages[0] - except: - return messages - - def add_deliveries(self, deliveries): - """ - >>> client.add_deliveries([{ - 'start': '2014-05-07T00:42:07', - 'messageId': 'xxxxx-xxxx-xxxxx', - 'fromName': 'John Doe', - 'fromEmail': 'test@example.com', - 'recipients': [{ - 'type': 'contact', - 'id': 'xxxxx-xxxx-xxxxx' - }], - 'type': 'transactional', - 'fields': [{ - 'name': 'link', - 'type': 'html', - 'content': 'link' - }, { - 'name': 'message', - 'type': 'html', - 'content': 'Dynamic message!!!' - }] - }]) - >>> - - The timezone of the start date is considered to be the one you have setup - in your bronto account if you don't specify it. If you want to use UTC, - you can use: - from datetime import datetime - datetime.strftime(datetime.utcnow(), '%FT%T+00:00') - - For more details: http://dev.bronto.com/api/v4/data-format - """ - required_attributes = ['start', 'messageId', 'type', 'fromEmail', - # 'replyEmail', Even if the doc says so, it's not required - 'fromName', 'recipients'] - final_deliveries = [] - for delivery in deliveries: - if not all(key in delivery for key in required_attributes): - raise ValueError('The attributes %s are required.' - % required_attributes) - delivery_obj = self._client.factory.create('deliveryObject') - for attribute, value in six.iteritems(delivery): - if attribute not in self._valid_delivery_fields: - raise KeyError('Invalid list attribute: %s' % attribute) - else: - setattr(delivery_obj, attribute, value) - final_deliveries.append(delivery_obj) - try: - response = self._client.service.addDeliveries(final_deliveries) - if hasattr(response, 'errors'): - err_str = ', '.join(['%s: %s' % (response.results[x].errorCode, - response.results[x].errorString) - for x in response.errors]) - raise BrontoException('An error occurred while adding deliveries: %s' - % err_str) - except WebFault as e: - raise BrontoException(e.message) - return response - - def add_delivery(self, delivery): - request = self.add_deliveries([delivery, ]) - try: - return request.results[0] - except: - return request.results - - def get_workflows(self, workflows=[]): - """ - - """ - final_workflows = [] - cached = [] - filter_operator = self._client.factory.create('filterOperator') - fop = filter_operator.EqualTo - for workflow in workflows: - if workflow in self._cached_workflows: - cached.append(self._cached_workflows[workflow]) - else: - workflow_string = self._client.factory.create('stringValue') - workflow_string.operator = fop - workflow_string.value = workflow - final_workflows.append(workflow_string) - if workflows and not final_workflows: - return [self._cached_workflows.get(workflow) for workflow in workflows] - - workflow_filter = self._client.factory.create('workflowFilter') - workflow_filter.name = final_workflows - filter_type = self._client.factory.create('filterType') - workflow_filter.type = filter_type.OR - - if not self._cached_all_workflows: - try: - response = self._client.service.readWorkflows(workflow_filter, - pageNumber=1) - for workflow in response: - self._cached_workflows[workflow.name] = workflow - if not len(final_workflows): - self._cached_all_workflows = True - except WebFault as e: - raise BrontoException(e.message) - else: - if not workflows: - response = [y for x, y in six.iteritems(self._cached_workflows)] - else: - response = [] - return response + cached - - def get_workflow(self, workflow): - """ - workflow: {'id':'myworkflowid','name':'name'} can search by name or by ID - """ - request = self.get_workflows([workflow, ]) - try: - return request[0] - except: - return request.results - - def get_workflow_by_id(self, id): - request = self.get_workflows_by_id([id]) - try: - return request[0] - except: - return [] - - def get_workflows_by_id(self, ids): - """ - - """ - workflow_filter = self._client.factory.create('workflowFilter') - workflow_filter.id = ids - filter_type = self._client.factory.create('filterType') - workflow_filter.type = filter_type.OR - workflow_results = [] - response = [] - if not self._cached_all_workflows: - workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if - getattr(workflow, 'id', None) in ids] - if len(ids) == len(workflow_results): - return workflow_results - try: - response = self._client.service.readWorkflows(filter=workflow_filter, pageNumber=1) - for workflow in response: - self._cached_workflows[workflow.name] = workflow - except WebFault as e: - raise BrontoException(e.message) - else: - workflow_results = [workflow for name, workflow in self._cached_workflows.iteritems() if - getattr(workflow, 'id', None) in ids] - return workflow_results + response - - # return [self._cached_workflows.get(workflow.name) for workflow in self._cached_workflows.items() if workflow.id in ids] - - def add_contacts_to_workflow(self, email, workflow_id): - workflow = self._client.factory.create('workflowObject') - contact = self._client.factory.create('contactObject') - workflow.id = workflow_id - contact.email = email - try: - response = self._client.service.addContactsToWorkflow(workflow, contact) - except WebFault as e: - raise BrontoException(e.message) - return response diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 55a2df3..0000000 --- a/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -suds