Skip to content

Commit 884b8b0

Browse files
authored
(For Jintae or Gary) Add session level decision. (#55)
* Add session level decision.
1 parent 2e15117 commit 884b8b0

File tree

4 files changed

+143
-6
lines changed

4 files changed

+143
-6
lines changed

CHANGES.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,9 @@
1+
3.2.0.0 2018-02-12
2+
==================
3+
4+
- Add session level decisions in Apply Decisions APIs.
5+
- Add support for filtering get decisions by entity type session.
6+
17
3.1.0.0 2017-01-17
28
==================
39

sift/client.py

Lines changed: 47 additions & 4 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}
321+
entity_type: only return decisions applicable to entity type {USER|ORDER|SESSION}
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']:
338-
raise ApiException("entity_type must be one of {user, order}")
337+
or entity_type.lower() not in ['user', 'order', 'session']:
338+
raise ApiException("entity_type must be one of {user, order, session}")
339339

340340
params['entity_type'] = entity_type
341341

@@ -395,7 +395,7 @@ def apply_order_decision(self, user_id, order_id, properties, timeout=None):
395395
user_id: id of user
396396
order_id: id of order
397397
properties:
398-
decision_id: decision to apply to user
398+
decision_id: decision to apply to order
399399
source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK}
400400
analyst: id or email, required if 'source: MANUAL_REVIEW'
401401
description: free form text (optional)
@@ -502,6 +502,46 @@ def get_order_decisions(self, order_id, timeout=None):
502502
raise ApiException(str(e))
503503

504504

505+
def apply_session_decision(self, user_id, session_id, properties, timeout=None):
506+
"""Apply decision to session
507+
508+
Args:
509+
user_id: id of user
510+
session_id: id of session
511+
properties:
512+
decision_id: decision to apply to session
513+
source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK}
514+
analyst: id or email, required if 'source: MANUAL_REVIEW'
515+
description: free form text (optional)
516+
time: in millis when decision was applied (optional)
517+
Returns
518+
A sift.client.Response object if the call succeeded, else raises an ApiException
519+
"""
520+
521+
if timeout is None:
522+
timeout = self.timeout
523+
524+
525+
if session_id is None or not isinstance(session_id, self.UNICODE_STRING) or \
526+
len(session_id.strip()) == 0:
527+
raise ApiException("session_id must be a string")
528+
529+
self._validate_apply_decision_request(properties, user_id)
530+
531+
try:
532+
return Response(requests.post(
533+
self._session_apply_decisions_url(self.account_id, user_id, session_id),
534+
data=json.dumps(properties),
535+
auth=requests.auth.HTTPBasicAuth(self.api_key, ''),
536+
headers={'Content-type': 'application/json',
537+
'Accept': '*/*',
538+
'User-Agent': self._user_agent()},
539+
timeout=timeout))
540+
541+
except requests.exceptions.RequestException as e:
542+
raise ApiException(str(e))
543+
544+
505545
def _user_agent(self):
506546
return 'SiftScience/v%s sift-python/%s' % (sift.version.API_VERSION, sift.version.VERSION)
507547

@@ -529,6 +569,9 @@ def _order_decisions_url(self, account_id, order_id):
529569
def _order_apply_decisions_url(self, account_id, user_id, order_id):
530570
return API3_URL + '/v3/accounts/%s/users/%s/orders/%s/decisions' % (account_id, user_id, order_id)
531571

572+
def _session_apply_decisions_url(self, account_id, user_id, session_id):
573+
return API3_URL + '/v3/accounts/%s/users/%s/sessions/%s/decisions' % (account_id, user_id, session_id)
574+
532575
class Response(object):
533576

534577
HTTP_CODES_WITHOUT_BODY = [204, 304]

sift/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
VERSION = '3.1.0.0'
1+
VERSION = '3.2.0.0'
22
API_VERSION = '204'

tests/test_client.py

Lines changed: 89 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,52 @@ def test_get_decisions(self):
346346
assert(response.is_ok())
347347
assert(response.body['data'][0]['id'] == 'block_user')
348348

349+
def test_get_decisions_entity_session(self):
350+
mock_response = mock.Mock()
351+
get_decisions_response_json = \
352+
'{' \
353+
'"data": [' \
354+
'{' \
355+
'"id": "block_session",' \
356+
'"name" : "Block session",' \
357+
'"description": "session has problems",' \
358+
'"entity_type": "session",' \
359+
'"abuse_type": "legacy",' \
360+
'"category": "block",' \
361+
'"webhook_url": "http://web.hook",' \
362+
'"created_at": "1468005577348",' \
363+
'"created_by": "admin@biz.com",' \
364+
'"updated_at": "1469229177756",' \
365+
'"updated_by": "analyst@biz.com"' \
366+
'}' \
367+
'],' \
368+
'"has_more": "true",' \
369+
'"next_ref": "v3/accounts/accountId/decisions"' \
370+
'}'
371+
372+
mock_response.content = get_decisions_response_json
373+
mock_response.json.return_value = json.loads(mock_response.content)
374+
mock_response.status_code = 200
375+
mock_response.headers = response_with_data_header()
376+
with mock.patch('requests.get') as mock_get:
377+
mock_get.return_value = mock_response
378+
379+
response = self.sift_client.get_decisions(entity_type="session",
380+
limit=10,
381+
start_from=None,
382+
abuse_types="account_takeover",
383+
timeout=3)
384+
mock_get.assert_called_with(
385+
'https://api3.siftscience.com/v3/accounts/ACCT/decisions',
386+
headers=mock.ANY,
387+
auth=mock.ANY,
388+
params={'entity_type':'session','limit':10,'abuse_types':'account_takeover'},
389+
timeout=3)
390+
391+
assert(isinstance(response, sift.client.Response))
392+
assert(response.is_ok())
393+
assert(response.body['data'][0]['id'] == 'block_session')
394+
349395
def test_apply_decision_to_user_ok(self):
350396
user_id = '54321'
351397
mock_response = mock.Mock()
@@ -394,12 +440,18 @@ def test_validate_no_user_id_string_fails(self):
394440
except Exception as e:
395441
assert(isinstance(e, sift.client.ApiException))
396442

397-
def test_apply_decision_to_order(self):
443+
def test_apply_decision_to_order_fails_with_no_order_id(self):
398444
try:
399445
self.sift_client.apply_order_decision("user_id", None, {})
400446
except Exception as e:
401447
assert(isinstance(e, sift.client.ApiException))
402448

449+
def test_apply_decision_to_session_fails_with_no_session_id(self):
450+
try:
451+
self.sift_client.apply_session_decision("user_id", None, {})
452+
except Exception as e:
453+
assert(isinstance(e, sift.client.ApiException))
454+
403455
def test_validate_apply_decision_request_no_analyst_fails(self):
404456
apply_decision_request = {
405457
'decision_id': 'user_looks_ok_legacy',
@@ -522,6 +574,42 @@ def test_apply_decision_to_order_ok(self):
522574
assert(response.http_status_code == 200)
523575
assert(response.body['entity']['type'] == 'order')
524576

577+
def test_apply_decision_to_session_ok(self):
578+
user_id = '54321'
579+
session_id = 'gigtleqddo84l8cm15qe4il'
580+
mock_response = mock.Mock()
581+
apply_decision_request = {
582+
'decision_id': 'session_looks_bad_ato',
583+
'source': 'AUTOMATED_RULE',
584+
'time': 1481569575
585+
}
586+
587+
apply_decision_response_json = '{' \
588+
'"entity": {' \
589+
'"id": "54321",' \
590+
'"type": "login"' \
591+
'},' \
592+
'"decision": {' \
593+
'"id":"session_looks_bad_ato"' \
594+
'},' \
595+
'"time":"1481569575"}'
596+
597+
mock_response.content = apply_decision_response_json
598+
mock_response.json.return_value = json.loads(mock_response.content)
599+
mock_response.status_code = 200
600+
mock_response.headers = response_with_data_header()
601+
with mock.patch('requests.post') as mock_post:
602+
mock_post.return_value = mock_response
603+
response = self.sift_client.apply_session_decision(user_id, session_id, apply_decision_request)
604+
data = json.dumps(apply_decision_request)
605+
mock_post.assert_called_with(
606+
'https://api3.siftscience.com/v3/accounts/ACCT/users/%s/sessions/%s/decisions' % (user_id,session_id),
607+
auth=mock.ANY, data=data, headers=mock.ANY, timeout=mock.ANY)
608+
assert(isinstance(response, sift.client.Response))
609+
assert(response.is_ok())
610+
assert(response.http_status_code == 200)
611+
assert(response.body['entity']['type'] == 'login')
612+
525613
def test_label_user_ok(self):
526614
user_id = '54321'
527615
mock_response = mock.Mock()

0 commit comments

Comments
 (0)