Repository URL to install this package:
Version:
0.32.0 ▾
|
import logging
import datetime
from django.conf import settings
from django.utils import dateparse
import pytz
from dockerhub.marketo import decorators
from dockerhub.marketo import constants
from dockerhub.marketo.client import MarketoClient
from dockerhub.marketo.exceptions import (
UnableToCompleteRequestException, MarketoImproperlyConfigured,
UnknownActivityTypeException, InvalidActivityDateException)
from dockerhub.marketo.utils import flatten
log = logging.getLogger(__name__)
VALID_LEAD_FIELDS = ('hubUsername', 'plan_type', 'email', 'firstName',
'lastName', 'company', 'country', 'postalCode',
'address', 'address2', 'city', 'partnerValue',
'accept_eval_terms', 'accept_eval_terms_date',
'accept_eusa_terms', 'accept_eusa_terms_date',
'docker_index_add_organization',
'docker_index_plan_downgrade',
'docker_index_plan_subscription',
'docker_index_plan_termination',
'docker_index_plan_upgrade',
'Docker_Hub_User_Name__c', 'phone', 'state',
'jobFunction', 'dockerUUID', 'title', 'leadStatus', 'unsubscribed')
VALID_ATTRIBUTE_FIELDS = ('Type', 'SubType', 'UserName', 'UUID', 'Other1',
'Other2')
class MarketoClientConfig(object):
"""
Instantiates a marketo client as part of object creation, looking
to the django settings if values are not passed directly.
"""
def __init__(self, host=None, client_id=None, client_secret=None,
track_api_usage=False):
self._host = host or getattr(settings, 'MARKETO_HOST')
if self._host is None:
raise MarketoImproperlyConfigured('MARKETO_HOST')
self._client_id = client_id or getattr(settings, 'MARKETO_CLIENT_ID')
if self._client_id is None:
raise MarketoImproperlyConfigured('MARKETO_CLIENT_ID')
self._client_secret = client_secret or getattr(
settings, 'MARKETO_SECRET'
)
if self._client_secret is None:
raise MarketoImproperlyConfigured('MARKETO_SECRET')
self._track_api_usage = (
track_api_usage or
getattr(settings, 'MARKETO_TRACK_API_USAGE', False))
self._client = MarketoClient(
host=self._host,
client_id=self._client_id,
client_secret=self._client_secret
)
class MarketoMailingListImpl(MarketoClientConfig):
"""
Implementation for high level interactions with Marketo.
"""
@decorators.track_api_calls
def is_subscribed(self, mailing_list, email):
"""
Check to see if the email address is on the mailing list specified
:param mailing_list: integer id for the mailing list
:param email: email address
:return: True if the email is on the mailing list, False otherwise.
:raises UnableToCompleteRequestException: when there's an issue
completing the request.
"""
try:
leads = self._client.find_lead(email)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction failed
message = "Unable to verify if {0} is_subscribed to {1}".format(
email, mailing_list
)
log.exception(message)
raise UnableToCompleteRequestException(message)
else:
try:
return self._client.is_member_of_list(mailing_list, leads)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction
# failed
message = "Unable to verify if {0} is_subscribed to " \
"{1}".format(email, mailing_list)
log.exception(message)
raise UnableToCompleteRequestException(message)
@decorators.track_api_calls
def subscribe(self, mailing_list, email):
"""
subscribes the email address to the mailing_list.
:param mailing_list: integer id for the mailing list
:param email: email address
:return: Returns True if the email is subscribed, False otherwise
:raises UnableToCompleteRequestException: when there's an issue
completing the request.
"""
try:
leads = self._client.find_lead(email)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction failed
message = "Unable to subscribe {0} to mailinglist {1}".format(
email, mailing_list
)
log.exception(message)
raise UnableToCompleteRequestException(message)
else:
try:
return self._client.add_leads_to_list(mailing_list, leads)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction
# failed
message = "Unable to subscribe {0} to mailinglist {1}".format(
email, mailing_list
)
log.exception(message)
raise UnableToCompleteRequestException(message)
@decorators.track_api_calls
def unsubscribe(self, mailing_list, email):
"""
Unsubscribes the email address from the mailing list
:param mailing_list: integer id for the mailing list
:param email: email address
:return: Returns True if the email is unsubscribed, False otherwise
:raises UnableToCompleteRequestException: when there's an issue
completing the request.
"""
try:
leads = self._client.find_lead(email)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction failed
message = "Unable to unsubscribe {0} from mailinglist {1}".format(
email, mailing_list
)
log.exception(message)
raise UnableToCompleteRequestException(message)
else:
try:
return self._client.remove_leads_from_list(mailing_list, leads)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction
# failed
message = "Unable to unsubscribe {0} from mailinglist " \
"{1}".format(email, mailing_list)
log.exception(message)
raise UnableToCompleteRequestException(message)
class LeadManagerImpl(MarketoClientConfig):
"""
Object used for lead management
"""
@decorators.track_api_calls
def create_lead(self, email, extra_values=None, cookie=None):
"""
Creates a lead if one doesn't exists, adding any additional
extra_values to the lead. If a cookie is passed, then
additional calls are made to get the lead id and link
the cookie to the newly created lead.
:param email: email address
:param extra_values: dictionary of extra values added to the lead
record
:param cookie: optional value that can be associated to a lead
:return: an array of lead ids associated to the email
:raises UnableToCompleteRequestException: when there's an issue
completing the request.
"""
if extra_values is not None:
extra_values = LeadManagerImpl._scrub_extra_values(extra_values)
lead_ids = []
try:
lead_ids = self._client.create_lead(
email,
extra_values=extra_values
)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction failed
message = "Unable to create lead: {0}".format(email)
log.exception(message)
raise UnableToCompleteRequestException(message)
else:
if cookie is not None:
# Most likely, we will only get one result, but we should
# handle multiple since it's possible to get back more
for lead_id in lead_ids:
try:
# link the lead to the cookie
self._client.associate_lead(lead_id, cookie)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the
# transaction failed
message = "Unable to associate lead {0} to cookie " \
"{1}".format(lead_id, cookie)
log.exception(message)
raise UnableToCompleteRequestException(message)
return lead_ids
@decorators.track_api_calls
def delete_lead(self, email):
"""
USED IN INTEGRATION TESTS, NOT FULLY TESTED
Deletes leads associated to specified email
:param email: email address
:return: True if leads are removed, false otherwise
:raises UnableToCompleteRequestException: when there's an issue
completing the request.
"""
try:
leads = self.get_lead_ids_by_email(email)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction failed
message = "Unable to get lead ids for {0}".format(email)
log.exception(message)
raise UnableToCompleteRequestException(message)
try:
self._client.delete_leads(leads)
except Exception as exc: # pylint: disable=W0703
message = "Unable to delete leads with ids {0} related to " \
"email {1}".format(leads, email)
log.exception(message)
raise UnableToCompleteRequestException(message)
return True
@decorators.track_api_calls
def create_activity(self, email, activity, activity_date,
attributes=None):
"""
Creates activity for the lead specified
:param email: email address of the lead
:param activity: DTR_BINARY_DOWNLOAD or DTRL_LICENSE_DOWNLOAD
:param activity_date: datetime the activity occurred
:param attributes: dictionary of activity attributes
:return: True if the activity is created, False otherwise
:raises UnknownActivityTypeException: when an unknown activity type is
passed
:raises UnableToCompleteRequestException: when there's an issue
completing the request.
:raises InvalidActivityDateException: when the activity date is
invalid, None, decodes to None, or causes a ValueError when
parsing
"""
# attribute default values
attributes = {} if attributes is None else attributes
# only continue if the activity type is valid
try:
activity = constants.MARKETO_ACTIVITY_TYPES[activity]
except KeyError:
raise UnknownActivityTypeException(activity)
# each marketo attribute should have a "Name" and "Value"
# component. Convert to the marketo standard, dropping any
# unexpected "Name" key/values
attributes = (
LeadManagerImpl._convert_keyvals_to_custom_activity_format(
attributes
)
)
activity_date = (
LeadManagerImpl._convert_date_to_custom_activity_format(
activity_date
)
)
# find the lead, creating one if there isn't one tied
# to the email address
try:
leads = self._client.find_lead(email)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the
# transaction failed
message = "Unable to get lead ids for {0}".format(email)
log.exception(message)
raise UnableToCompleteRequestException(message)
# it's possible that multiple leads are returned
# so we need to handle the case, however this is
# unlikely
for lead in leads:
try:
# client api call to create activity
self._client.create_custom_activity(
lead,
activity.id,
activity.value,
activity_date,
attributes=attributes
)
except Exception: # pylint: disable=W0703
# We're casting a wide net here and assume the
# transaction failed
message = "Unable to get lead ids for {0}".format(email)
log.exception(message)
raise UnableToCompleteRequestException(message)
return True
def get_lead_ids_by_email(self, email):
"""
Common access for retrieving lead ids associated to an email
:param email: email address
:return: array of integer leads
"""
log.debug("Looking for leads with email: %s", email)
try:
leads = self._client.get_leads(
MarketoClient.FilterType.EMAIL,
values=[email, ],
fields=[MarketoClient.FieldType.ID, ]
)
except Exception as exc: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction failed
message = "Unable to get lead ids for {0}".format(email)
log.exception(message)
raise UnableToCompleteRequestException(message)
log.debug("Found leads as: %s", leads)
return [int(lead) for lead in flatten('id', leads)]
@staticmethod
def _scrub_extra_values(extra_values):
"""
The api only supports a specific set of fields. We'll scrub the input
and send only the fields they expect to see. If there's any input
that's not expected, we should emit a warning, because we should
probably update the code sending the unknown value
:return:
"""
scrubbed_values = {}
for k, v in extra_values.iteritems():
if k in VALID_LEAD_FIELDS:
# ignore empty values to avoid overwriting
# data collected previously
if v:
scrubbed_values[k] = v
else:
log.warning("Found unexpected extra value key as %s. "
"Ignoring.", k)
return scrubbed_values
@staticmethod
def _convert_keyvals_to_custom_activity_format(attributes):
"""
converts the dict into an object with name and value components. If
there is an attribute key not within the valid attribute field list,
a warning is emitted and the value is dropped
ex:
this:
{
'SubType': '{Ubuntu, RHEL/CentOS}'
}
to this:
{
"name": "SubType",
"value": "{Ubuntu, RHEL/CentOS}"
}
"""
validated_attributes = []
for k, v in attributes.iteritems():
if k in VALID_ATTRIBUTE_FIELDS:
validated_attributes.append({'name': k, 'value': v})
else:
log.warning("Found unexpected attribute key as %s. "
"Ignoring.", k)
return validated_attributes
@staticmethod
def _convert_date_to_custom_activity_format(activity_date):
"""
Converts a string formatted datetime or a datetime object (both
tz aware and tz naive) into the format required for the custom
activity
:param activity_date: datetime of the activity
:return: formatted datetime string
"""
log.debug("Date before conversion: {0}".format(activity_date))
orig_activity_date = activity_date
try:
if not isinstance(activity_date, datetime.datetime):
activity_date = dateparse.parse_datetime(activity_date)
log.debug("Parsed activity date: %s", activity_date)
except (TypeError, ValueError, AttributeError):
raise InvalidActivityDateException(activity_date)
if activity_date is None:
raise InvalidActivityDateException(
"Invalid date converted to None: %s", orig_activity_date)
if activity_date.tzinfo is not None:
# convert to UTC time
activity_date = activity_date.astimezone(pytz.utc)
log.debug("Converted datetime to UTC: %s", activity_date)
# remove timezone component from date - marketo doesn't understand
# this component :/
activity_date = activity_date.replace(tzinfo=None)
log.debug("Stripped datetime of its tzinfo: %s", activity_date)
# convert to a json serializable date string
activity_date = activity_date.isoformat()
log.debug("json formattable activity date: %s", activity_date)
return activity_date
class CampaignManagerImpl(MarketoClientConfig):
"""
Object used for campaign management
"""
@decorators.track_api_calls
def request_campaign(self, campaign_id, leads=None, tokens=None):
"""
The Smart Campaign must have a "Campaign is Requested" trigger with a Web Service API source.
A maximum of 100 lead records can be included in each call. If the campaign is not a valid
trigger campaign that is a child of a program and has the Campaign is Requested,
source Web Service API, the API will return an error code of 1003.
:param campaign_id: marketo campaign id
:param leads: Array of Lead IDs dictionaries (e.g. [{"id" : 123}, {"id" : 456}])
:param tokens: Array of name/value pairs (e.g. [{"{{my.message}}" : "Updated message",
{"{{my.other token}}" : Value for other token}])
:return: True if lead is updated, false otherwise
:raises UnableToCompleteRequestException: when there's an issue
completing the request.
"""
if leads is None or campaign_id is None:
message = "Missing parameters: leads = {0}, campaign_id = {1} ".format(
leads, campaign_id)
log.exception(message)
raise UnableToCompleteRequestException(message)
try:
result = self._client.request_campaign(
campaign_id,
leads=leads,
tokens=tokens,
)
except: # pylint: disable=W0703
# We're casting a wide net here and assume the transaction failed
message = "Unable to send the campaign request. leads = {0}, campaign_id = {1}".format(
leads, campaign_id)
log.exception(message)
raise UnableToCompleteRequestException(message)
return result