Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1import json 

2import logging 

3from collections import namedtuple 

4 

5import requests 

6from retrying import retry 

7from sdc_etl_libs.api_helpers.APIUtils import \ 

8 datetime_to_microseconds_timestamp 

9from sdc_etl_libs.api_helpers.OAuthAPI import OAuthAPI 

10from sdc_etl_libs.api_helpers.SDCAPIExceptions import AccessException 

11from sdc_etl_libs.sdc_dataframe.Dataframe import Dataframe 

12 

13if len(logging.getLogger().handlers) > 0: 

14 logging.getLogger().setLevel(logging.INFO) 

15else: 

16 logging.basicConfig(level=logging.INFO) 

17 

18 

19class EnhancedConversionAlphaAPI(OAuthAPI): 

20 """ 

21 API Connector to Enhanced Conversion (EC) Apha Api 

22 """ 

23 

24 def __init__(self, endpoint_schema_: dict = None, **kwargs): 

25 self.endpoint_schema = endpoint_schema_ 

26 self.method = self.endpoint_schema["info"]["opts"].get('api_call_details', {}).get('method') 

27 self.base_url = self.endpoint_schema['info']['access']['base_url'] 

28 self.access_token_url = self.endpoint_schema.get('info').get('access').get('token_url') 

29 credentials = self.endpoint_schema['info']['access']['credentials'] 

30 auth_ = kwargs.get("auth_", {}) 

31 

32 super().__init__( 

33 refresh_token_=auth_.get('refresh_token'), 

34 client_id_=auth_.get('client_id'), 

35 client_secret_=auth_.get('client_secret'), 

36 credential_type_=credentials.get('type'), 

37 credential_id_=credentials.get('opts').get('name'), 

38 region_=endpoint_schema_.get('region', 'us-east-2')) 

39 

40 if self.base_url is None: 

41 raise ValueError('"base_url" was not provided in endpoint_schema') 

42 

43 if self.method is None: 

44 raise ValueError('"api_method" was not provided in endpoint_schema') 

45 

46 self.get_new_access_token(self.client_id, self.client_secret, self.refresh_token, self.access_token_url) 

47 

48 @staticmethod 

49 def _validate_nan_value(value_): 

50 """ 

51 This method change a nan values by None value. 

52 :param: value_: Any value of the conversion data 

53 :rtype: object 

54 """ 

55 return None if (str(value_) == "nan") else value_ 

56 

57 def __build_request_data(self, conversion_data_: dict): 

58 """ 

59 This method generate a tuple with the required values to post the information to EC API 

60 :param conversion_data_: conversion data 

61 :type: dict 

62 :rtype: Tuple 

63 """ 

64 requestData = namedtuple('RequestData', 'headers body params') 

65 conversion_time_in_microseconds = datetime_to_microseconds_timestamp(conversion_data_.get("CONVERSION_TIME")) 

66 

67 header = {'Content-Type': 'application/json', 'Authorization': f"Bearer {self.access_token}"} 

68 params = { 

69 "conversion_time": conversion_time_in_microseconds, 

70 "label": conversion_data_.get("LABEL"), 

71 "value": conversion_data_.get("VALUE"), 

72 "conversion_tracking_id": conversion_data_.get("CONVERSION_TRACKING_ID"), 

73 "oid": conversion_data_.get("OID"), 

74 } 

75 

76 address_fields = [("hashed_first_name", "ADDRESS_HASHED_FIRST_NAME"), 

77 ("hashed_last_name", "ADDRESS_HASHED_LAST_NAME"), 

78 ("hashed_street_address", "ADDRESS_HASHED_STREET_ADDRESS"), ("city", "ADDRESS_CITY"), 

79 ("region", "ADDRESS_REGION"), ("postcode", "ADDRESS_POSTAL_CODE"), 

80 ("country", "ADDRESS_COUNTRY")] 

81 

82 # Generate a dictionaty with the address_fields that contains values. 

83 address = list( 

84 map(lambda address_field: (address_field[0], conversion_data_.get(address_field[1])), 

85 filter(lambda f: self._validate_nan_value(conversion_data_.get(f[1])) is not None, address_fields))) 

86 

87 body = {"pii_data": {"hashed_email": conversion_data_.get("HASHED_EMAIL"), "address": address}} 

88 return requestData(header, body, params) 

89 

90 @staticmethod 

91 def __retry_if_access_error(exception): 

92 """ 

93 Auxiliar function to check for a given exception. Used by @retry decoration function. 

94 :return is_access_error(bool): True if we should retry (in this case when it's an AccessException), False 

95 otherwise 

96 """ 

97 return isinstance(exception, AccessException) 

98 

99 @retry(retry_on_exception=__retry_if_access_error, stop_max_attempt_number=3, wait_fixed=2000) 

100 def request_data(self, conversion_data_: dict): 

101 """ 

102 This method posts a conversion to EC API 

103 :param conversion_data_ conversion data 

104 :type: dict 

105 :rtype: object 

106 """ 

107 logging.info("Sending conversion to the EC API. Hashed_email: %s ", conversion_data_.get("HASHED_EMAIL")) 

108 try: 

109 request_data_tuple = self.__build_request_data(conversion_data_) 

110 response = requests.request( 

111 method="POST", 

112 url=self.base_url, 

113 headers=request_data_tuple.headers, 

114 params=request_data_tuple.params, 

115 data=json.dumps(request_data_tuple.body)) 

116 

117 if response.status_code == 401: 

118 logging.warning("Response payload: \n%s", json.dumps(json.loads(response.text), indent=2)) 

119 logging.warning("Trying to refresh access token...") 

120 self.reset_access_token() 

121 except Exception as err: 

122 logging.error("Error Sending conversion to the EC API. %s Hashed_email", conversion_data_.get("HASHED_EMAIL")) 

123 raise ValueError(f'Call failed: {err}') 

124 

125 def write_data(self, sdc_dataframe_: Dataframe): 

126 """ 

127 Sends the specified data_ to the EC API using one of the available methods 

128 :param sdc_dataframe_ : conversions to be sent to the EC API 

129 :type: Pandas Dataframe 

130 """ 

131 logging.info("Sending data to the Enhanced Conversion Apha API - Method %s", self.method) 

132 if self.method == "post_events": 

133 conversions = sdc_dataframe_.df.T.to_dict().values() 

134 for conversion in conversions: 

135 self.request_data(conversion)