We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
TypeError when SFMC error is returned
SFMC response.content is bytes, but the sfmc.py module tries to concat that message with strings to throw NewsletterExceptions. 

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `basket/news/backends/sfmc.py`
Content:
```
1 """
2 API Client Library for Salesforce Marketing Cloud (SFMC)
3 Formerly ExactTarget
4 """
5 from random import randint
6 from time import time
7 
8 from django.conf import settings
9 from django.core.cache import cache
10 
11 import requests
12 from django_statsd.clients import statsd
13 from FuelSDK import ET_Client, ET_DataExtension_Row, ET_TriggeredSend
14 
15 from basket.news.backends.common import get_timer_decorator, NewsletterException, \
16                                  NewsletterNoResultsException
17 
18 
19 time_request = get_timer_decorator('news.backends.sfmc')
20 
21 
22 HERD_TIMEOUT = 60
23 AUTH_BUFFER = 300  # 5 min
24 MAX_BUFFER = HERD_TIMEOUT + AUTH_BUFFER
25 
26 
27 class ETRefreshClient(ET_Client):
28     token_cache_key = 'backends:sfmc:auth:tokens'
29     authTokenExpiresIn = None
30     token_property_names = [
31         'authToken',
32         'authTokenExpiration',
33         'internalAuthToken',
34         'refreshKey',
35     ]
36     _old_authToken = None
37 
38     def __init__(self, get_server_wsdl=False, debug=False, params=None):
39         # setting this manually as it has thrown errors and doesn't change
40         if settings.USE_SANDBOX_BACKEND:
41             self.endpoint = 'https://webservice.test.exacttarget.com/Service.asmx'
42         else:
43             self.endpoint = 'https://webservice.s4.exacttarget.com/Service.asmx'
44 
45         super(ETRefreshClient, self).__init__(get_server_wsdl, debug, params)
46 
47     def token_is_expired(self):
48         """Report token is expired between 5 and 6 minutes early
49 
50         Having the expiration be random helps prevent multiple basket
51         instances simultaneously requesting a new token from SFMC,
52         a.k.a. the Thundering Herd problem.
53         """
54         if self.authTokenExpiration is None:
55             return True
56 
57         time_buffer = randint(1, HERD_TIMEOUT) + AUTH_BUFFER
58         return time() + time_buffer > self.authTokenExpiration
59 
60     def refresh_auth_tokens_from_cache(self):
61         """Refresh the auth token and other values from cache"""
62         if self.authToken is not None and time() + MAX_BUFFER < self.authTokenExpiration:
63             # no need to refresh if the current tokens are still good
64             return
65 
66         tokens = cache.get(self.token_cache_key)
67         if tokens:
68             if not isinstance(tokens, dict):
69                 # something wrong was cached
70                 cache.delete(self.token_cache_key)
71                 return
72 
73             for prop, value in tokens.items():
74                 if prop in self.token_property_names:
75                     setattr(self, prop, value)
76 
77             # set the value so we can detect if it changed later
78             self._old_authToken = self.authToken
79             self.build_soap_client()
80 
81     def cache_auth_tokens(self):
82         if self.authToken is not None and self.authToken != self._old_authToken:
83             new_tokens = {prop: getattr(self, prop) for prop in self.token_property_names}
84             # 10 min longer than expiration so that refreshKey can be used
85             cache.set(self.token_cache_key, new_tokens, self.authTokenExpiresIn + 600)
86 
87     def request_token(self, payload):
88         r = requests.post(self.auth_url, json=payload)
89         try:
90             token_response = r.json()
91         except ValueError:
92             raise NewsletterException('SFMC Error During Auth: ' + r.content,
93                                       status_code=r.status_code)
94 
95         if 'accessToken' in token_response:
96             return token_response
97 
98         # try again without refreshToken
99         if 'refreshToken' in payload:
100             # not strictly required, makes testing easier
101             payload = payload.copy()
102             del payload['refreshToken']
103             return self.request_token(payload)
104 
105         raise NewsletterException('SFMC Error During Auth: ' + r.content,
106                                   status_code=r.status_code)
107 
108     def refresh_token(self, force_refresh=False):
109         """
110         Called from many different places right before executing a SOAP call
111         """
112         # If we don't already have a token or the token expires within 5 min(300 seconds), get one
113         self.refresh_auth_tokens_from_cache()
114         if force_refresh or self.authToken is None or self.token_is_expired():
115             payload = {
116                 'clientId': self.client_id,
117                 'clientSecret': self.client_secret,
118                 'accessType': 'offline',
119             }
120             if self.refreshKey:
121                 payload['refreshToken'] = self.refreshKey
122 
123             token_response = self.request_token(payload)
124             statsd.incr('news.backends.sfmc.auth_token_refresh')
125             self.authToken = token_response['accessToken']
126             self.authTokenExpiresIn = token_response['expiresIn']
127             self.authTokenExpiration = time() + self.authTokenExpiresIn
128             self.internalAuthToken = token_response['legacyToken']
129             if 'refreshToken' in token_response:
130                 self.refreshKey = token_response['refreshToken']
131 
132             self.build_soap_client()
133             self.cache_auth_tokens()
134 
135 
136 def assert_response(resp):
137     if not resp.status:
138         raise NewsletterException(str(resp.results))
139 
140 
141 def assert_results(resp):
142     assert_response(resp)
143     if not resp.results:
144         raise NewsletterNoResultsException()
145 
146 
147 def build_attributes(data):
148     return [{'Name': key, 'Value': value} for key, value in data.items()]
149 
150 
151 class SFMC(object):
152     _client = None
153     sms_api_url = 'https://www.exacttargetapis.com/sms/v1/messageContact/{}/send'
154     rowset_api_url = 'https://www.exacttargetapis.com/hub/v1/dataevents/key:{}/rowset'
155 
156     @property
157     def client(self):
158         if self._client is None and 'clientid' in settings.SFMC_SETTINGS:
159             self._client = ETRefreshClient(False, settings.SFMC_DEBUG, settings.SFMC_SETTINGS)
160 
161         return self._client
162 
163     @property
164     def auth_header(self):
165         self.client.refresh_token()
166         return {'Authorization': 'Bearer {0}'.format(self.client.authToken)}
167 
168     def _get_row_obj(self, de_name, props):
169         row = ET_DataExtension_Row()
170         row.auth_stub = self.client
171         row.CustomerKey = row.Name = de_name
172         row.props = props
173         return row
174 
175     @time_request
176     def get_row(self, de_name, fields, token=None, email=None):
177         """
178         Get the values of `fields` from a data extension. Either token or email is required.
179 
180         @param de_name: name of the data extension
181         @param fields: list of column names
182         @param token: the user's token
183         @param email: the user's email address
184         @return: dict of user data
185         """
186         assert token or email, 'token or email required'
187         row = self._get_row_obj(de_name, fields)
188         if token:
189             row.search_filter = {
190                 'Property': 'TOKEN',
191                 'SimpleOperator': 'equals',
192                 'Value': token,
193             }
194         elif email:
195             row.search_filter = {
196                 'Property': 'EMAIL_ADDRESS_',
197                 'SimpleOperator': 'equals',
198                 'Value': email,
199             }
200 
201         resp = row.get()
202         assert_results(resp)
203         # TODO do something if more than 1 result is returned
204         return dict((p.Name, p.Value)
205                     for p in resp.results[0].Properties.Property)
206 
207     @time_request
208     def add_row(self, de_name, values):
209         """
210         Add a row to a data extension.
211 
212         @param de_name: name of the data extension
213         @param values: dict containing the COLUMN: VALUE pairs
214         @return: None
215         """
216         row = self._get_row_obj(de_name, values)
217         resp = row.post()
218         assert_response(resp)
219 
220     @time_request
221     def update_row(self, de_name, values):
222         """
223         Update a row in a data extension.
224 
225         @param de_name: name of the data extension
226         @param values: dict containing the COLUMN: VALUE pairs.
227             Must contain TOKEN or EMAIL_ADDRESS_.
228         @return: None
229         """
230         row = self._get_row_obj(de_name, values)
231         resp = row.patch()
232         assert_response(resp)
233 
234     @time_request
235     def upsert_row(self, de_name, values):
236         """
237         Add or update a row in a data extension.
238 
239         @param de_name: name of the data extension
240         @param values: dict containing the COLUMN: VALUE pairs.
241             Must contain TOKEN or EMAIL_ADDRESS_.
242         @return: None
243         """
244         row = self._get_row_obj(de_name, values)
245         resp = row.patch(True)
246         assert_response(resp)
247 
248     @time_request
249     def delete_row(self, de_name, column, value):
250         """
251         Delete a row from a data extension. Either token or email are required.
252 
253         @param de_name: name of the data extension
254         @param token: user's token
255         @param email: user's email address
256         @return: None
257         """
258         row = self._get_row_obj(de_name, {column: value})
259         resp = row.delete()
260         assert_response(resp)
261 
262     @time_request
263     def send_mail(self, ts_name, email, subscriber_key, token=None):
264         """
265         Send an email message to a user (Triggered Send).
266 
267         @param ts_name: the name of the message to send
268         @param email: the email address of the user
269         @param subscriber_key: the key for the user in SFMC
270         @param format: T or H for Text or HTML
271         @param token: optional token if a recovery message
272         @return: None
273         """
274         ts = ET_TriggeredSend()
275         ts.auth_stub = self.client
276         ts.props = {'CustomerKey': ts_name}
277         subscriber = {
278             'EmailAddress': email,
279             'SubscriberKey': subscriber_key,
280         }
281         if token:
282             ts.attributes = build_attributes({
283                 'Token__c': token,
284             })
285             subscriber['Attributes'] = ts.attributes
286         ts.subscribers = [subscriber]
287         resp = ts.send()
288         assert_response(resp)
289 
290     @time_request
291     def send_sms(self, phone_numbers, message_id):
292         if isinstance(phone_numbers, str):
293             phone_numbers = [phone_numbers]
294 
295         phone_numbers = [pn.lstrip('+') for pn in phone_numbers]
296         data = {
297             'mobileNumbers': phone_numbers,
298             'Subscribe': True,
299             'Resubscribe': True,
300             'keyword': 'FFDROID',  # TODO: Set keyword in arguments.
301         }
302         url = self.sms_api_url.format(message_id)
303         response = requests.post(url, json=data, headers=self.auth_header, timeout=10)
304         if response.status_code >= 500:
305             raise NewsletterException('SFMC Server Error: {}'.format(response.content),
306                                       status_code=response.status_code)
307 
308         if response.status_code >= 400:
309             raise NewsletterException('SFMC Request Error: {}'.format(response.content),
310                                       status_code=response.status_code)
311 
312     @time_request
313     def bulk_upsert_rows(self, de_name, values):
314         url = self.rowset_api_url.format(de_name)
315         response = requests.post(url, json=values, headers=self.auth_header, timeout=30)
316         if response.status_code >= 500:
317             raise NewsletterException('SFMC Server Error: {}'.format(response.content),
318                                       status_code=response.status_code)
319 
320         if response.status_code >= 400:
321             raise NewsletterException(response.content, status_code=response.status_code)
322 
323 
324 sfmc = SFMC()
325
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/basket/news/backends/sfmc.py b/basket/news/backends/sfmc.py
--- a/basket/news/backends/sfmc.py
+++ b/basket/news/backends/sfmc.py
@@ -7,6 +7,7 @@
 
 from django.conf import settings
 from django.core.cache import cache
+from django.utils.encoding import force_str
 
 import requests
 from django_statsd.clients import statsd
@@ -89,7 +90,7 @@
         try:
             token_response = r.json()
         except ValueError:
-            raise NewsletterException('SFMC Error During Auth: ' + r.content,
+            raise NewsletterException('SFMC Error During Auth: ' + force_str(r.content),
                                       status_code=r.status_code)
 
         if 'accessToken' in token_response:
@@ -102,7 +103,7 @@
             del payload['refreshToken']
             return self.request_token(payload)
 
-        raise NewsletterException('SFMC Error During Auth: ' + r.content,
+        raise NewsletterException('SFMC Error During Auth: ' + force_str(r.content),
                                   status_code=r.status_code)
 
     def refresh_token(self, force_refresh=False):
@@ -302,11 +303,11 @@
         url = self.sms_api_url.format(message_id)
         response = requests.post(url, json=data, headers=self.auth_header, timeout=10)
         if response.status_code >= 500:
-            raise NewsletterException('SFMC Server Error: {}'.format(response.content),
+            raise NewsletterException('SFMC Server Error: {}'.format(force_str(response.content)),
                                       status_code=response.status_code)
 
         if response.status_code >= 400:
-            raise NewsletterException('SFMC Request Error: {}'.format(response.content),
+            raise NewsletterException('SFMC Request Error: {}'.format(force_str(response.content)),
                                       status_code=response.status_code)
 
     @time_request
@@ -314,11 +315,11 @@
         url = self.rowset_api_url.format(de_name)
         response = requests.post(url, json=values, headers=self.auth_header, timeout=30)
         if response.status_code >= 500:
-            raise NewsletterException('SFMC Server Error: {}'.format(response.content),
+            raise NewsletterException('SFMC Server Error: {}'.format(force_str(response.content)),
                                       status_code=response.status_code)
 
         if response.status_code >= 400:
-            raise NewsletterException(response.content, status_code=response.status_code)
+            raise NewsletterException(force_str(response.content), status_code=response.status_code)
 
 
 sfmc = SFMC()

{"golden_diff": "diff --git a/basket/news/backends/sfmc.py b/basket/news/backends/sfmc.py\n--- a/basket/news/backends/sfmc.py\n+++ b/basket/news/backends/sfmc.py\n@@ -7,6 +7,7 @@\n \n from django.conf import settings\n from django.core.cache import cache\n+from django.utils.encoding import force_str\n \n import requests\n from django_statsd.clients import statsd\n@@ -89,7 +90,7 @@\n         try:\n             token_response = r.json()\n         except ValueError:\n-            raise NewsletterException('SFMC Error During Auth: ' + r.content,\n+            raise NewsletterException('SFMC Error During Auth: ' + force_str(r.content),\n                                       status_code=r.status_code)\n \n         if 'accessToken' in token_response:\n@@ -102,7 +103,7 @@\n             del payload['refreshToken']\n             return self.request_token(payload)\n \n-        raise NewsletterException('SFMC Error During Auth: ' + r.content,\n+        raise NewsletterException('SFMC Error During Auth: ' + force_str(r.content),\n                                   status_code=r.status_code)\n \n     def refresh_token(self, force_refresh=False):\n@@ -302,11 +303,11 @@\n         url = self.sms_api_url.format(message_id)\n         response = requests.post(url, json=data, headers=self.auth_header, timeout=10)\n         if response.status_code >= 500:\n-            raise NewsletterException('SFMC Server Error: {}'.format(response.content),\n+            raise NewsletterException('SFMC Server Error: {}'.format(force_str(response.content)),\n                                       status_code=response.status_code)\n \n         if response.status_code >= 400:\n-            raise NewsletterException('SFMC Request Error: {}'.format(response.content),\n+            raise NewsletterException('SFMC Request Error: {}'.format(force_str(response.content)),\n                                       status_code=response.status_code)\n \n     @time_request\n@@ -314,11 +315,11 @@\n         url = self.rowset_api_url.format(de_name)\n         response = requests.post(url, json=values, headers=self.auth_header, timeout=30)\n         if response.status_code >= 500:\n-            raise NewsletterException('SFMC Server Error: {}'.format(response.content),\n+            raise NewsletterException('SFMC Server Error: {}'.format(force_str(response.content)),\n                                       status_code=response.status_code)\n \n         if response.status_code >= 400:\n-            raise NewsletterException(response.content, status_code=response.status_code)\n+            raise NewsletterException(force_str(response.content), status_code=response.status_code)\n \n \n sfmc = SFMC()\n", "issue": "TypeError when SFMC error is returned\nSFMC response.content is bytes, but the sfmc.py module tries to concat that message with strings to throw NewsletterExceptions. \n", "before_files": [{"content": "\"\"\"\nAPI Client Library for Salesforce Marketing Cloud (SFMC)\nFormerly ExactTarget\n\"\"\"\nfrom random import randint\nfrom time import time\n\nfrom django.conf import settings\nfrom django.core.cache import cache\n\nimport requests\nfrom django_statsd.clients import statsd\nfrom FuelSDK import ET_Client, ET_DataExtension_Row, ET_TriggeredSend\n\nfrom basket.news.backends.common import get_timer_decorator, NewsletterException, \\\n                                 NewsletterNoResultsException\n\n\ntime_request = get_timer_decorator('news.backends.sfmc')\n\n\nHERD_TIMEOUT = 60\nAUTH_BUFFER = 300  # 5 min\nMAX_BUFFER = HERD_TIMEOUT + AUTH_BUFFER\n\n\nclass ETRefreshClient(ET_Client):\n    token_cache_key = 'backends:sfmc:auth:tokens'\n    authTokenExpiresIn = None\n    token_property_names = [\n        'authToken',\n        'authTokenExpiration',\n        'internalAuthToken',\n        'refreshKey',\n    ]\n    _old_authToken = None\n\n    def __init__(self, get_server_wsdl=False, debug=False, params=None):\n        # setting this manually as it has thrown errors and doesn't change\n        if settings.USE_SANDBOX_BACKEND:\n            self.endpoint = 'https://webservice.test.exacttarget.com/Service.asmx'\n        else:\n            self.endpoint = 'https://webservice.s4.exacttarget.com/Service.asmx'\n\n        super(ETRefreshClient, self).__init__(get_server_wsdl, debug, params)\n\n    def token_is_expired(self):\n        \"\"\"Report token is expired between 5 and 6 minutes early\n\n        Having the expiration be random helps prevent multiple basket\n        instances simultaneously requesting a new token from SFMC,\n        a.k.a. the Thundering Herd problem.\n        \"\"\"\n        if self.authTokenExpiration is None:\n            return True\n\n        time_buffer = randint(1, HERD_TIMEOUT) + AUTH_BUFFER\n        return time() + time_buffer > self.authTokenExpiration\n\n    def refresh_auth_tokens_from_cache(self):\n        \"\"\"Refresh the auth token and other values from cache\"\"\"\n        if self.authToken is not None and time() + MAX_BUFFER < self.authTokenExpiration:\n            # no need to refresh if the current tokens are still good\n            return\n\n        tokens = cache.get(self.token_cache_key)\n        if tokens:\n            if not isinstance(tokens, dict):\n                # something wrong was cached\n                cache.delete(self.token_cache_key)\n                return\n\n            for prop, value in tokens.items():\n                if prop in self.token_property_names:\n                    setattr(self, prop, value)\n\n            # set the value so we can detect if it changed later\n            self._old_authToken = self.authToken\n            self.build_soap_client()\n\n    def cache_auth_tokens(self):\n        if self.authToken is not None and self.authToken != self._old_authToken:\n            new_tokens = {prop: getattr(self, prop) for prop in self.token_property_names}\n            # 10 min longer than expiration so that refreshKey can be used\n            cache.set(self.token_cache_key, new_tokens, self.authTokenExpiresIn + 600)\n\n    def request_token(self, payload):\n        r = requests.post(self.auth_url, json=payload)\n        try:\n            token_response = r.json()\n        except ValueError:\n            raise NewsletterException('SFMC Error During Auth: ' + r.content,\n                                      status_code=r.status_code)\n\n        if 'accessToken' in token_response:\n            return token_response\n\n        # try again without refreshToken\n        if 'refreshToken' in payload:\n            # not strictly required, makes testing easier\n            payload = payload.copy()\n            del payload['refreshToken']\n            return self.request_token(payload)\n\n        raise NewsletterException('SFMC Error During Auth: ' + r.content,\n                                  status_code=r.status_code)\n\n    def refresh_token(self, force_refresh=False):\n        \"\"\"\n        Called from many different places right before executing a SOAP call\n        \"\"\"\n        # If we don't already have a token or the token expires within 5 min(300 seconds), get one\n        self.refresh_auth_tokens_from_cache()\n        if force_refresh or self.authToken is None or self.token_is_expired():\n            payload = {\n                'clientId': self.client_id,\n                'clientSecret': self.client_secret,\n                'accessType': 'offline',\n            }\n            if self.refreshKey:\n                payload['refreshToken'] = self.refreshKey\n\n            token_response = self.request_token(payload)\n            statsd.incr('news.backends.sfmc.auth_token_refresh')\n            self.authToken = token_response['accessToken']\n            self.authTokenExpiresIn = token_response['expiresIn']\n            self.authTokenExpiration = time() + self.authTokenExpiresIn\n            self.internalAuthToken = token_response['legacyToken']\n            if 'refreshToken' in token_response:\n                self.refreshKey = token_response['refreshToken']\n\n            self.build_soap_client()\n            self.cache_auth_tokens()\n\n\ndef assert_response(resp):\n    if not resp.status:\n        raise NewsletterException(str(resp.results))\n\n\ndef assert_results(resp):\n    assert_response(resp)\n    if not resp.results:\n        raise NewsletterNoResultsException()\n\n\ndef build_attributes(data):\n    return [{'Name': key, 'Value': value} for key, value in data.items()]\n\n\nclass SFMC(object):\n    _client = None\n    sms_api_url = 'https://www.exacttargetapis.com/sms/v1/messageContact/{}/send'\n    rowset_api_url = 'https://www.exacttargetapis.com/hub/v1/dataevents/key:{}/rowset'\n\n    @property\n    def client(self):\n        if self._client is None and 'clientid' in settings.SFMC_SETTINGS:\n            self._client = ETRefreshClient(False, settings.SFMC_DEBUG, settings.SFMC_SETTINGS)\n\n        return self._client\n\n    @property\n    def auth_header(self):\n        self.client.refresh_token()\n        return {'Authorization': 'Bearer {0}'.format(self.client.authToken)}\n\n    def _get_row_obj(self, de_name, props):\n        row = ET_DataExtension_Row()\n        row.auth_stub = self.client\n        row.CustomerKey = row.Name = de_name\n        row.props = props\n        return row\n\n    @time_request\n    def get_row(self, de_name, fields, token=None, email=None):\n        \"\"\"\n        Get the values of `fields` from a data extension. Either token or email is required.\n\n        @param de_name: name of the data extension\n        @param fields: list of column names\n        @param token: the user's token\n        @param email: the user's email address\n        @return: dict of user data\n        \"\"\"\n        assert token or email, 'token or email required'\n        row = self._get_row_obj(de_name, fields)\n        if token:\n            row.search_filter = {\n                'Property': 'TOKEN',\n                'SimpleOperator': 'equals',\n                'Value': token,\n            }\n        elif email:\n            row.search_filter = {\n                'Property': 'EMAIL_ADDRESS_',\n                'SimpleOperator': 'equals',\n                'Value': email,\n            }\n\n        resp = row.get()\n        assert_results(resp)\n        # TODO do something if more than 1 result is returned\n        return dict((p.Name, p.Value)\n                    for p in resp.results[0].Properties.Property)\n\n    @time_request\n    def add_row(self, de_name, values):\n        \"\"\"\n        Add a row to a data extension.\n\n        @param de_name: name of the data extension\n        @param values: dict containing the COLUMN: VALUE pairs\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, values)\n        resp = row.post()\n        assert_response(resp)\n\n    @time_request\n    def update_row(self, de_name, values):\n        \"\"\"\n        Update a row in a data extension.\n\n        @param de_name: name of the data extension\n        @param values: dict containing the COLUMN: VALUE pairs.\n            Must contain TOKEN or EMAIL_ADDRESS_.\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, values)\n        resp = row.patch()\n        assert_response(resp)\n\n    @time_request\n    def upsert_row(self, de_name, values):\n        \"\"\"\n        Add or update a row in a data extension.\n\n        @param de_name: name of the data extension\n        @param values: dict containing the COLUMN: VALUE pairs.\n            Must contain TOKEN or EMAIL_ADDRESS_.\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, values)\n        resp = row.patch(True)\n        assert_response(resp)\n\n    @time_request\n    def delete_row(self, de_name, column, value):\n        \"\"\"\n        Delete a row from a data extension. Either token or email are required.\n\n        @param de_name: name of the data extension\n        @param token: user's token\n        @param email: user's email address\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, {column: value})\n        resp = row.delete()\n        assert_response(resp)\n\n    @time_request\n    def send_mail(self, ts_name, email, subscriber_key, token=None):\n        \"\"\"\n        Send an email message to a user (Triggered Send).\n\n        @param ts_name: the name of the message to send\n        @param email: the email address of the user\n        @param subscriber_key: the key for the user in SFMC\n        @param format: T or H for Text or HTML\n        @param token: optional token if a recovery message\n        @return: None\n        \"\"\"\n        ts = ET_TriggeredSend()\n        ts.auth_stub = self.client\n        ts.props = {'CustomerKey': ts_name}\n        subscriber = {\n            'EmailAddress': email,\n            'SubscriberKey': subscriber_key,\n        }\n        if token:\n            ts.attributes = build_attributes({\n                'Token__c': token,\n            })\n            subscriber['Attributes'] = ts.attributes\n        ts.subscribers = [subscriber]\n        resp = ts.send()\n        assert_response(resp)\n\n    @time_request\n    def send_sms(self, phone_numbers, message_id):\n        if isinstance(phone_numbers, str):\n            phone_numbers = [phone_numbers]\n\n        phone_numbers = [pn.lstrip('+') for pn in phone_numbers]\n        data = {\n            'mobileNumbers': phone_numbers,\n            'Subscribe': True,\n            'Resubscribe': True,\n            'keyword': 'FFDROID',  # TODO: Set keyword in arguments.\n        }\n        url = self.sms_api_url.format(message_id)\n        response = requests.post(url, json=data, headers=self.auth_header, timeout=10)\n        if response.status_code >= 500:\n            raise NewsletterException('SFMC Server Error: {}'.format(response.content),\n                                      status_code=response.status_code)\n\n        if response.status_code >= 400:\n            raise NewsletterException('SFMC Request Error: {}'.format(response.content),\n                                      status_code=response.status_code)\n\n    @time_request\n    def bulk_upsert_rows(self, de_name, values):\n        url = self.rowset_api_url.format(de_name)\n        response = requests.post(url, json=values, headers=self.auth_header, timeout=30)\n        if response.status_code >= 500:\n            raise NewsletterException('SFMC Server Error: {}'.format(response.content),\n                                      status_code=response.status_code)\n\n        if response.status_code >= 400:\n            raise NewsletterException(response.content, status_code=response.status_code)\n\n\nsfmc = SFMC()\n", "path": "basket/news/backends/sfmc.py"}], "after_files": [{"content": "\"\"\"\nAPI Client Library for Salesforce Marketing Cloud (SFMC)\nFormerly ExactTarget\n\"\"\"\nfrom random import randint\nfrom time import time\n\nfrom django.conf import settings\nfrom django.core.cache import cache\nfrom django.utils.encoding import force_str\n\nimport requests\nfrom django_statsd.clients import statsd\nfrom FuelSDK import ET_Client, ET_DataExtension_Row, ET_TriggeredSend\n\nfrom basket.news.backends.common import get_timer_decorator, NewsletterException, \\\n                                 NewsletterNoResultsException\n\n\ntime_request = get_timer_decorator('news.backends.sfmc')\n\n\nHERD_TIMEOUT = 60\nAUTH_BUFFER = 300  # 5 min\nMAX_BUFFER = HERD_TIMEOUT + AUTH_BUFFER\n\n\nclass ETRefreshClient(ET_Client):\n    token_cache_key = 'backends:sfmc:auth:tokens'\n    authTokenExpiresIn = None\n    token_property_names = [\n        'authToken',\n        'authTokenExpiration',\n        'internalAuthToken',\n        'refreshKey',\n    ]\n    _old_authToken = None\n\n    def __init__(self, get_server_wsdl=False, debug=False, params=None):\n        # setting this manually as it has thrown errors and doesn't change\n        if settings.USE_SANDBOX_BACKEND:\n            self.endpoint = 'https://webservice.test.exacttarget.com/Service.asmx'\n        else:\n            self.endpoint = 'https://webservice.s4.exacttarget.com/Service.asmx'\n\n        super(ETRefreshClient, self).__init__(get_server_wsdl, debug, params)\n\n    def token_is_expired(self):\n        \"\"\"Report token is expired between 5 and 6 minutes early\n\n        Having the expiration be random helps prevent multiple basket\n        instances simultaneously requesting a new token from SFMC,\n        a.k.a. the Thundering Herd problem.\n        \"\"\"\n        if self.authTokenExpiration is None:\n            return True\n\n        time_buffer = randint(1, HERD_TIMEOUT) + AUTH_BUFFER\n        return time() + time_buffer > self.authTokenExpiration\n\n    def refresh_auth_tokens_from_cache(self):\n        \"\"\"Refresh the auth token and other values from cache\"\"\"\n        if self.authToken is not None and time() + MAX_BUFFER < self.authTokenExpiration:\n            # no need to refresh if the current tokens are still good\n            return\n\n        tokens = cache.get(self.token_cache_key)\n        if tokens:\n            if not isinstance(tokens, dict):\n                # something wrong was cached\n                cache.delete(self.token_cache_key)\n                return\n\n            for prop, value in tokens.items():\n                if prop in self.token_property_names:\n                    setattr(self, prop, value)\n\n            # set the value so we can detect if it changed later\n            self._old_authToken = self.authToken\n            self.build_soap_client()\n\n    def cache_auth_tokens(self):\n        if self.authToken is not None and self.authToken != self._old_authToken:\n            new_tokens = {prop: getattr(self, prop) for prop in self.token_property_names}\n            # 10 min longer than expiration so that refreshKey can be used\n            cache.set(self.token_cache_key, new_tokens, self.authTokenExpiresIn + 600)\n\n    def request_token(self, payload):\n        r = requests.post(self.auth_url, json=payload)\n        try:\n            token_response = r.json()\n        except ValueError:\n            raise NewsletterException('SFMC Error During Auth: ' + force_str(r.content),\n                                      status_code=r.status_code)\n\n        if 'accessToken' in token_response:\n            return token_response\n\n        # try again without refreshToken\n        if 'refreshToken' in payload:\n            # not strictly required, makes testing easier\n            payload = payload.copy()\n            del payload['refreshToken']\n            return self.request_token(payload)\n\n        raise NewsletterException('SFMC Error During Auth: ' + force_str(r.content),\n                                  status_code=r.status_code)\n\n    def refresh_token(self, force_refresh=False):\n        \"\"\"\n        Called from many different places right before executing a SOAP call\n        \"\"\"\n        # If we don't already have a token or the token expires within 5 min(300 seconds), get one\n        self.refresh_auth_tokens_from_cache()\n        if force_refresh or self.authToken is None or self.token_is_expired():\n            payload = {\n                'clientId': self.client_id,\n                'clientSecret': self.client_secret,\n                'accessType': 'offline',\n            }\n            if self.refreshKey:\n                payload['refreshToken'] = self.refreshKey\n\n            token_response = self.request_token(payload)\n            statsd.incr('news.backends.sfmc.auth_token_refresh')\n            self.authToken = token_response['accessToken']\n            self.authTokenExpiresIn = token_response['expiresIn']\n            self.authTokenExpiration = time() + self.authTokenExpiresIn\n            self.internalAuthToken = token_response['legacyToken']\n            if 'refreshToken' in token_response:\n                self.refreshKey = token_response['refreshToken']\n\n            self.build_soap_client()\n            self.cache_auth_tokens()\n\n\ndef assert_response(resp):\n    if not resp.status:\n        raise NewsletterException(str(resp.results))\n\n\ndef assert_results(resp):\n    assert_response(resp)\n    if not resp.results:\n        raise NewsletterNoResultsException()\n\n\ndef build_attributes(data):\n    return [{'Name': key, 'Value': value} for key, value in data.items()]\n\n\nclass SFMC(object):\n    _client = None\n    sms_api_url = 'https://www.exacttargetapis.com/sms/v1/messageContact/{}/send'\n    rowset_api_url = 'https://www.exacttargetapis.com/hub/v1/dataevents/key:{}/rowset'\n\n    @property\n    def client(self):\n        if self._client is None and 'clientid' in settings.SFMC_SETTINGS:\n            self._client = ETRefreshClient(False, settings.SFMC_DEBUG, settings.SFMC_SETTINGS)\n\n        return self._client\n\n    @property\n    def auth_header(self):\n        self.client.refresh_token()\n        return {'Authorization': 'Bearer {0}'.format(self.client.authToken)}\n\n    def _get_row_obj(self, de_name, props):\n        row = ET_DataExtension_Row()\n        row.auth_stub = self.client\n        row.CustomerKey = row.Name = de_name\n        row.props = props\n        return row\n\n    @time_request\n    def get_row(self, de_name, fields, token=None, email=None):\n        \"\"\"\n        Get the values of `fields` from a data extension. Either token or email is required.\n\n        @param de_name: name of the data extension\n        @param fields: list of column names\n        @param token: the user's token\n        @param email: the user's email address\n        @return: dict of user data\n        \"\"\"\n        assert token or email, 'token or email required'\n        row = self._get_row_obj(de_name, fields)\n        if token:\n            row.search_filter = {\n                'Property': 'TOKEN',\n                'SimpleOperator': 'equals',\n                'Value': token,\n            }\n        elif email:\n            row.search_filter = {\n                'Property': 'EMAIL_ADDRESS_',\n                'SimpleOperator': 'equals',\n                'Value': email,\n            }\n\n        resp = row.get()\n        assert_results(resp)\n        # TODO do something if more than 1 result is returned\n        return dict((p.Name, p.Value)\n                    for p in resp.results[0].Properties.Property)\n\n    @time_request\n    def add_row(self, de_name, values):\n        \"\"\"\n        Add a row to a data extension.\n\n        @param de_name: name of the data extension\n        @param values: dict containing the COLUMN: VALUE pairs\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, values)\n        resp = row.post()\n        assert_response(resp)\n\n    @time_request\n    def update_row(self, de_name, values):\n        \"\"\"\n        Update a row in a data extension.\n\n        @param de_name: name of the data extension\n        @param values: dict containing the COLUMN: VALUE pairs.\n            Must contain TOKEN or EMAIL_ADDRESS_.\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, values)\n        resp = row.patch()\n        assert_response(resp)\n\n    @time_request\n    def upsert_row(self, de_name, values):\n        \"\"\"\n        Add or update a row in a data extension.\n\n        @param de_name: name of the data extension\n        @param values: dict containing the COLUMN: VALUE pairs.\n            Must contain TOKEN or EMAIL_ADDRESS_.\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, values)\n        resp = row.patch(True)\n        assert_response(resp)\n\n    @time_request\n    def delete_row(self, de_name, column, value):\n        \"\"\"\n        Delete a row from a data extension. Either token or email are required.\n\n        @param de_name: name of the data extension\n        @param token: user's token\n        @param email: user's email address\n        @return: None\n        \"\"\"\n        row = self._get_row_obj(de_name, {column: value})\n        resp = row.delete()\n        assert_response(resp)\n\n    @time_request\n    def send_mail(self, ts_name, email, subscriber_key, token=None):\n        \"\"\"\n        Send an email message to a user (Triggered Send).\n\n        @param ts_name: the name of the message to send\n        @param email: the email address of the user\n        @param subscriber_key: the key for the user in SFMC\n        @param format: T or H for Text or HTML\n        @param token: optional token if a recovery message\n        @return: None\n        \"\"\"\n        ts = ET_TriggeredSend()\n        ts.auth_stub = self.client\n        ts.props = {'CustomerKey': ts_name}\n        subscriber = {\n            'EmailAddress': email,\n            'SubscriberKey': subscriber_key,\n        }\n        if token:\n            ts.attributes = build_attributes({\n                'Token__c': token,\n            })\n            subscriber['Attributes'] = ts.attributes\n        ts.subscribers = [subscriber]\n        resp = ts.send()\n        assert_response(resp)\n\n    @time_request\n    def send_sms(self, phone_numbers, message_id):\n        if isinstance(phone_numbers, str):\n            phone_numbers = [phone_numbers]\n\n        phone_numbers = [pn.lstrip('+') for pn in phone_numbers]\n        data = {\n            'mobileNumbers': phone_numbers,\n            'Subscribe': True,\n            'Resubscribe': True,\n            'keyword': 'FFDROID',  # TODO: Set keyword in arguments.\n        }\n        url = self.sms_api_url.format(message_id)\n        response = requests.post(url, json=data, headers=self.auth_header, timeout=10)\n        if response.status_code >= 500:\n            raise NewsletterException('SFMC Server Error: {}'.format(force_str(response.content)),\n                                      status_code=response.status_code)\n\n        if response.status_code >= 400:\n            raise NewsletterException('SFMC Request Error: {}'.format(force_str(response.content)),\n                                      status_code=response.status_code)\n\n    @time_request\n    def bulk_upsert_rows(self, de_name, values):\n        url = self.rowset_api_url.format(de_name)\n        response = requests.post(url, json=values, headers=self.auth_header, timeout=30)\n        if response.status_code >= 500:\n            raise NewsletterException('SFMC Server Error: {}'.format(force_str(response.content)),\n                                      status_code=response.status_code)\n\n        if response.status_code >= 400:\n            raise NewsletterException(force_str(response.content), status_code=response.status_code)\n\n\nsfmc = SFMC()\n", "path": "basket/news/backends/sfmc.py"}]}