Skip to content

Commit ef7edf7

Browse files
authored
(Gary L.) Adding Content-level decisions, incrementing version number (#56)
* Squashed commits, added get content decision status, and incremented python version * Added updated content decision status request * Updated README * Removed a new line
1 parent 884b8b0 commit ef7edf7

File tree

5 files changed

+169
-17
lines changed

5 files changed

+169
-17
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
4.0.0.0 2018-02-23
2+
==================
3+
4+
- V205 APIs are now called by default -- this is an incompatible change
5+
(use version='204' to call the previous API version)
6+
17
3.2.0.0 2018-02-12
28
==================
39

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,14 @@ try:
143143
except sift.client.ApiException:
144144
# request failed
145145

146+
147+
148+
# Get the latest decisions for a piece of content
149+
try:
150+
response = client.get_content_decisions('example_user', 'example_content');
151+
except sift.client.ApiException:
152+
# request failed
153+
146154
```
147155

148156

sift/client.py

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ def get_decisions(self, entity_type, limit=None, start_from=None, abuse_types=No
318318
"""Get decisions available to customer
319319
320320
Args:
321-
entity_type: only return decisions applicable to entity type {USER|ORDER|SESSION}
321+
entity_type: only return decisions applicable to entity type {USER|ORDER|SESSION|CONTENT}
322322
limit: number of query results (decisions) to return [optional, default: 100]
323323
start_from: result set offset for use in pagination [optional, default: 0]
324324
abuse_types: comma-separated list of abuse_types used to filter returned decisions (optional)
@@ -334,8 +334,8 @@ def get_decisions(self, entity_type, limit=None, start_from=None, abuse_types=No
334334
params = {}
335335

336336
if not isinstance(entity_type, self.UNICODE_STRING) or len(entity_type.strip()) == 0 \
337-
or entity_type.lower() not in ['user', 'order', 'session']:
338-
raise ApiException("entity_type must be one of {user, order, session}")
337+
or entity_type.lower() not in ['user', 'order', 'session', 'content']:
338+
raise ApiException("entity_type must be one of {user, order, session, content}")
339339

340340
params['entity_type'] = entity_type
341341

@@ -502,6 +502,38 @@ def get_order_decisions(self, order_id, timeout=None):
502502
raise ApiException(str(e))
503503

504504

505+
def get_content_decisions(self, user_id, content_id, timeout=None):
506+
"""Gets the decisions for a piece of content.
507+
508+
Args:
509+
user_id: The ID of the owner of the content.
510+
content_id: The ID of a piece of content.
511+
512+
Returns:
513+
A sift.client.Response object if the call succeeded.
514+
Otherwise, raises an ApiException.
515+
516+
"""
517+
if not isinstance(content_id, self.UNICODE_STRING) or len(content_id.strip()) == 0:
518+
raise ApiException("content_id must be a string")
519+
520+
if not isinstance(user_id, self.UNICODE_STRING) or len(user_id.strip()) == 0:
521+
raise ApiException("user_id must be a string")
522+
523+
if timeout is None:
524+
timeout = self.timeout
525+
526+
try:
527+
return Response(requests.get(
528+
self._content_decisions_url(self.account_id, user_id, content_id),
529+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
530+
headers={'User-Agent': self._user_agent()},
531+
timeout=timeout))
532+
533+
except requests.exceptions.RequestException as e:
534+
raise ApiException(str(e))
535+
536+
505537
def apply_session_decision(self, user_id, session_id, properties, timeout=None):
506538
"""Apply decision to session
507539
@@ -542,6 +574,46 @@ def apply_session_decision(self, user_id, session_id, properties, timeout=None):
542574
raise ApiException(str(e))
543575

544576

577+
def apply_content_decision(self, user_id, content_id, properties, timeout=None):
578+
"""Apply decision to content
579+
580+
Args:
581+
user_id: id of user
582+
content_id: id of content
583+
properties:
584+
decision_id: decision to apply to session
585+
source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK}
586+
analyst: id or email, required if 'source: MANUAL_REVIEW'
587+
description: free form text (optional)
588+
time: in millis when decision was applied (optional)
589+
Returns
590+
A sift.client.Response object if the call succeeded, else raises an ApiException
591+
"""
592+
593+
if timeout is None:
594+
timeout = self.timeout
595+
596+
597+
if content_id is None or not isinstance(content_id, self.UNICODE_STRING) or \
598+
len(content_id.strip()) == 0:
599+
raise ApiException("content_id must be a string")
600+
601+
self._validate_apply_decision_request(properties, user_id)
602+
603+
try:
604+
return Response(requests.post(
605+
self._content_apply_decisions_url(self.account_id, user_id, content_id),
606+
data=json.dumps(properties),
607+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
608+
headers={'Content-type': 'application/json',
609+
'Accept': '*/*',
610+
'User-Agent': self._user_agent()},
611+
timeout=timeout))
612+
613+
except requests.exceptions.RequestException as e:
614+
raise ApiException(str(e))
615+
616+
545617
def _user_agent(self):
546618
return 'SiftScience/v%s sift-python/%s' % (sift.version.API_VERSION, sift.version.VERSION)
547619

@@ -566,12 +638,18 @@ def _user_decisions_url(self, account_id, user_id):
566638
def _order_decisions_url(self, account_id, order_id):
567639
return API3_URL + '/v3/accounts/%s/orders/%s/decisions' % (account_id, order_id)
568640

641+
def _content_decisions_url(self, account_id, user_id, content_id):
642+
return API3_URL + '/v3/accounts/%s/users/%s/content/%s/decisions' % (account_id, user_id, content_id)
643+
569644
def _order_apply_decisions_url(self, account_id, user_id, order_id):
570645
return API3_URL + '/v3/accounts/%s/users/%s/orders/%s/decisions' % (account_id, user_id, order_id)
571646

572647
def _session_apply_decisions_url(self, account_id, user_id, session_id):
573648
return API3_URL + '/v3/accounts/%s/users/%s/sessions/%s/decisions' % (account_id, user_id, session_id)
574649

650+
def _content_apply_decisions_url(self, account_id, user_id, content_id):
651+
return API3_URL + '/v3/accounts/%s/users/%s/content/%s/decisions' % (account_id, user_id, content_id)
652+
575653
class Response(object):
576654

577655
HTTP_CODES_WITHOUT_BODY = [204, 304]

sift/version.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
VERSION = '3.2.0.0'
2-
API_VERSION = '204'
1+
VERSION = '4.0.0.0'
2+
API_VERSION = '205'

tests/test_client.py

Lines changed: 72 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -189,7 +189,7 @@ def test_event_ok(self):
189189
mock_post.return_value = mock_response
190190
response = self.sift_client.track(event, valid_transaction_properties())
191191
mock_post.assert_called_with(
192-
'https://api.siftscience.com/v204/events',
192+
'https://api.siftscience.com/v205/events',
193193
data=mock.ANY,
194194
headers=mock.ANY,
195195
timeout=mock.ANY,
@@ -212,7 +212,7 @@ def test_event_with_timeout_param_ok(self):
212212
response = self.sift_client.track(
213213
event, valid_transaction_properties(), timeout=test_timeout)
214214
mock_post.assert_called_with(
215-
'https://api.siftscience.com/v204/events',
215+
'https://api.siftscience.com/v205/events',
216216
data=mock.ANY,
217217
headers=mock.ANY,
218218
timeout=test_timeout,
@@ -232,7 +232,7 @@ def test_score_ok(self):
232232
mock_post.return_value = mock_response
233233
response = self.sift_client.score('12345')
234234
mock_post.assert_called_with(
235-
'https://api.siftscience.com/v204/score/12345',
235+
'https://api.siftscience.com/v205/score/12345',
236236
params={'api_key': self.test_key},
237237
headers=mock.ANY,
238238
timeout=mock.ANY)
@@ -254,7 +254,7 @@ def test_score_with_timeout_param_ok(self):
254254
mock_post.return_value = mock_response
255255
response = self.sift_client.score('12345', test_timeout)
256256
mock_post.assert_called_with(
257-
'https://api.siftscience.com/v204/score/12345',
257+
'https://api.siftscience.com/v205/score/12345',
258258
params={'api_key': self.test_key},
259259
headers=mock.ANY,
260260
timeout=test_timeout)
@@ -281,7 +281,7 @@ def test_sync_score_ok(self):
281281
return_score=True,
282282
abuse_types=['payment_abuse', 'content_abuse', 'legacy'])
283283
mock_post.assert_called_with(
284-
'https://api.siftscience.com/v204/events',
284+
'https://api.siftscience.com/v205/events',
285285
data=mock.ANY,
286286
headers=mock.ANY,
287287
timeout=mock.ANY,
@@ -452,6 +452,12 @@ def test_apply_decision_to_session_fails_with_no_session_id(self):
452452
except Exception as e:
453453
assert(isinstance(e, sift.client.ApiException))
454454

455+
def test_apply_decision_to_content_fails_with_no_content_id(self):
456+
try:
457+
self.sift_client.apply_content_decision("user_id", None, {})
458+
except Exception as e:
459+
assert(isinstance(e, sift.client.ApiException))
460+
455461
def test_validate_apply_decision_request_no_analyst_fails(self):
456462
apply_decision_request = {
457463
'decision_id': 'user_looks_ok_legacy',
@@ -610,6 +616,42 @@ def test_apply_decision_to_session_ok(self):
610616
assert(response.http_status_code == 200)
611617
assert(response.body['entity']['type'] == 'login')
612618

619+
def test_apply_decision_to_content_ok(self):
620+
user_id = '54321'
621+
content_id = 'listing-1231'
622+
mock_response = mock.Mock()
623+
apply_decision_request = {
624+
'decision_id': 'content_looks_bad_content_abuse',
625+
'source': 'AUTOMATED_RULE',
626+
'time': 1481569575
627+
}
628+
629+
apply_decision_response_json = '{' \
630+
'"entity": {' \
631+
'"id": "54321",' \
632+
'"type": "create_content"' \
633+
'},' \
634+
'"decision": {' \
635+
'"id":"content_looks_bad_content_abuse"' \
636+
'},' \
637+
'"time":"1481569575"}'
638+
639+
mock_response.content = apply_decision_response_json
640+
mock_response.json.return_value = json.loads(mock_response.content)
641+
mock_response.status_code = 200
642+
mock_response.headers = response_with_data_header()
643+
with mock.patch('requests.post') as mock_post:
644+
mock_post.return_value = mock_response
645+
response = self.sift_client.apply_content_decision(user_id, content_id, apply_decision_request)
646+
data = json.dumps(apply_decision_request)
647+
mock_post.assert_called_with(
648+
'https://api3.siftscience.com/v3/accounts/ACCT/users/%s/content/%s/decisions' % (user_id,content_id),
649+
auth=mock.ANY, data=data, headers=mock.ANY, timeout=mock.ANY)
650+
assert(isinstance(response, sift.client.Response))
651+
assert(response.is_ok())
652+
assert(response.http_status_code == 200)
653+
assert(response.body['entity']['type'] == 'create_content')
654+
613655
def test_label_user_ok(self):
614656
user_id = '54321'
615657
mock_response = mock.Mock()
@@ -630,7 +672,7 @@ def test_label_user_ok(self):
630672
properties.update({'$api_key': self.test_key, '$type': '$label'})
631673
data = json.dumps(properties)
632674
mock_post.assert_called_with(
633-
'https://api.siftscience.com/v204/users/%s/labels' % user_id,
675+
'https://api.siftscience.com/v205/users/%s/labels' % user_id,
634676
data=data, headers=mock.ANY, timeout=mock.ANY, params={})
635677
assert(isinstance(response, sift.client.Response))
636678
assert(response.is_ok())
@@ -659,7 +701,7 @@ def test_label_user_with_timeout_param_ok(self):
659701
properties.update({'$api_key': self.test_key, '$type': '$label'})
660702
data = json.dumps(properties)
661703
mock_post.assert_called_with(
662-
'https://api.siftscience.com/v204/users/%s/labels' % user_id,
704+
'https://api.siftscience.com/v205/users/%s/labels' % user_id,
663705
data=data, headers=mock.ANY, timeout=test_timeout, params={})
664706
assert(isinstance(response, sift.client.Response))
665707
assert(response.is_ok())
@@ -674,7 +716,7 @@ def test_unlabel_user_ok(self):
674716
mock_delete.return_value = mock_response
675717
response = self.sift_client.unlabel(user_id, abuse_type='account_abuse')
676718
mock_delete.assert_called_with(
677-
'https://api.siftscience.com/v204/users/%s/labels' % user_id,
719+
'https://api.siftscience.com/v205/users/%s/labels' % user_id,
678720
headers=mock.ANY,
679721
timeout=mock.ANY,
680722
params={'api_key': self.test_key, 'abuse_type': 'account_abuse'})
@@ -714,7 +756,7 @@ def test_unlabel_user_with_special_chars_ok(self):
714756
mock_delete.return_value = mock_response
715757
response = self.sift_client.unlabel(user_id)
716758
mock_delete.assert_called_with(
717-
'https://api.siftscience.com/v204/users/%s/labels' % urllib.quote(user_id),
759+
'https://api.siftscience.com/v205/users/%s/labels' % urllib.quote(user_id),
718760
headers=mock.ANY,
719761
timeout=mock.ANY,
720762
params={'api_key': self.test_key})
@@ -742,7 +784,7 @@ def test_label_user__with_special_chars_ok(self):
742784
properties.update({'$api_key': self.test_key, '$type': '$label'})
743785
data = json.dumps(properties)
744786
mock_post.assert_called_with(
745-
'https://api.siftscience.com/v204/users/%s/labels' % urllib.quote(user_id),
787+
'https://api.siftscience.com/v205/users/%s/labels' % urllib.quote(user_id),
746788
data=data,
747789
headers=mock.ANY,
748790
timeout=mock.ANY,
@@ -763,7 +805,7 @@ def test_score__with_special_user_id_chars_ok(self):
763805
mock_post.return_value = mock_response
764806
response = self.sift_client.score(user_id, abuse_types=['legacy'])
765807
mock_post.assert_called_with(
766-
'https://api.siftscience.com/v204/score/%s' % urllib.quote(user_id),
808+
'https://api.siftscience.com/v205/score/%s' % urllib.quote(user_id),
767809
params={'api_key': self.test_key, 'abuse_types': 'legacy'},
768810
headers=mock.ANY,
769811
timeout=mock.ANY)
@@ -820,7 +862,7 @@ def test_return_actions_on_track(self):
820862
response = self.sift_client.track(
821863
event, valid_transaction_properties(), return_action=True)
822864
mock_post.assert_called_with(
823-
'https://api.siftscience.com/v204/events',
865+
'https://api.siftscience.com/v205/events',
824866
data=mock.ANY,
825867
headers=mock.ANY,
826868
timeout=mock.ANY,
@@ -895,6 +937,24 @@ def test_get_order_decisions(self):
895937
assert(response.body['decisions']['payment_abuse']['decision']['id'] == 'decision7')
896938
assert(response.body['decisions']['promotion_abuse']['decision']['id'] == 'good_order')
897939

940+
def test_get_content_decisions(self):
941+
mock_response = mock.Mock()
942+
mock_response.content = '{"decisions":{"content_abuse":{"decision":{"id":"content_looks_bad_content_abuse"},"time":1468517407135,"webhook_succeeded":true}}}'
943+
mock_response.json.return_value = json.loads(mock_response.content)
944+
mock_response.status_code = 200
945+
mock_response.headers = response_with_data_header()
946+
947+
with mock.patch('requests.get') as mock_get:
948+
mock_get.return_value = mock_response
949+
950+
response = self.sift_client.get_content_decisions('example_user', 'example_content')
951+
mock_get.assert_called_with(
952+
'https://api3.siftscience.com/v3/accounts/ACCT/users/example_user/content/example_content/decisions',
953+
headers=mock.ANY, auth=mock.ANY, timeout=mock.ANY)
954+
955+
assert(isinstance(response, sift.client.Response))
956+
assert(response.is_ok())
957+
assert(response.body['decisions']['content_abuse']['decision']['id'] == 'content_looks_bad_content_abuse')
898958

899959
def main():
900960
unittest.main()

0 commit comments

Comments
 (0)