From 24b1dba848c92fedd4e0fecdd3ade6631f58c096 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Mon, 24 Mar 2025 16:04:45 +0100 Subject: [PATCH 01/16] Add typing. Apply code style. Update documentation --- .flake8 | 5 + .gitignore | 1 + .pre-commit-config.yaml | 24 + .travis.yml | 21 +- CHANGES.md | 10 +- CONTRIBUTING.md | 0 pyproject.toml | 68 + requirements-dev.txt | 5 + setup.py | 71 - sift/__init__.py | 10 +- sift/client.py | 1962 ++++++++----- sift/constants.py | 7 + sift/exceptions.py | 24 + sift/utils.py | 20 + sift/version.py | 4 +- .../decisions_api/test_decisions_api.py | 107 +- .../events_api/test_events_api.py | 2556 ++++++++--------- test_integration_app/globals.py | 10 +- test_integration_app/main.py | 182 +- .../psp_merchant_api/test_psp_merchant_api.py | 124 +- .../score_api/test_score_api.py | 27 +- .../test_verification_api.py | 88 +- .../workflows_api/test_workflows_api.py | 34 +- tests/test_client.py | 1524 ++++++---- tests/test_client_v203.py | 523 ++-- tests/test_verification_apis.py | 102 +- 26 files changed, 4261 insertions(+), 3248 deletions(-) create mode 100644 .flake8 create mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 pyproject.toml create mode 100644 requirements-dev.txt delete mode 100644 setup.py create mode 100644 sift/constants.py create mode 100644 sift/exceptions.py create mode 100644 sift/utils.py diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..321faca --- /dev/null +++ b/.flake8 @@ -0,0 +1,5 @@ +[flake8] +ignore = E501,W503 +per-file-ignores = __init__.py:F401 +max-line-length = 79 +disable-noqa = true \ No newline at end of file diff --git a/.gitignore b/.gitignore index 15fafde..ebc2184 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +__pycache__ *.py[cod] # C extensions diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..087e858 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +repos: + - repo: https://github.com/crate-ci/typos + rev: v1.30.2 + hooks: + - id: typos + args: [ --force-exclude ] + - repo: https://github.com/pycqa/isort + rev: 5.13.2 + hooks: + - id: isort + - repo: https://github.com/psf/black + rev: 24.8.0 + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: 7.1.2 + hooks: + - id: flake8 + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.14.1 + hooks: + - id: mypy + args: [ --install-types, --non-interactive ] + additional_dependencies: [ types-requests ] diff --git a/.travis.yml b/.travis.yml index 98c2bd6..ff6e263 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,10 +1,23 @@ language: python python: - - "2.7" - - "3.4" + - "3.8" + - "3.9" + - "3.10" + - "3.11" + - "3.12" + - "3.13" +before_install: + - python --version + - pip install -U pip # command to install dependencies install: - - pip install -e .[test] + - pip install -e . + - pip install -r requirements-dev.txt # command to run tests script: - - unit2 + - flake8 --count + - black --check . + - isort --check . + - mypy --install-types --non-interactive . + - typos + - python -m unittest discover diff --git a/CHANGES.md b/CHANGES.md index 0b4536c..349c7a0 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,11 @@ +6.0.0 Unreleased +================ +- Support for Python 3.13 + +INCOMPATIBLE CHANGES INTRODUCED IN 6.0.0: + +- Removed support for Python < 3.8 + 5.6.1 2024-10-08 - Updated implementation to use Basic Authentication instead of passing `API_KEY` as a request parameter for the following calls: - `client.score()` @@ -127,7 +135,7 @@ INCOMPATIBLE CHANGES INTRODUCED IN API V205: 1.1.2.0 (2015-02-04) ==================== -- Added Unlabel functionaly +- Added Unlabel functionality - Minor bug fixes. 1.1.1.0 (2014-09-3) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..e69de29 diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..9ebb193 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,68 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "Sift" +version = "6.0.0" +authors = [ + {name = "Sift Science", email = "support@siftscience.com"}, +] +description = "Python bindings for Sift Science's API" +readme = "README.md" +license = {file = "LICENSE"} +classifiers=[ + "Development Status :: 5 - Production/Stable", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Operating System :: OS Independent", + "Typing :: Typed", + "License :: OSI Approved :: MIT License", +] +keywords = ["sift", "sift-python"] +requires-python = ">= 3.8" +dependencies = [ + "requests < 3.0.0", +] + +[project.urls] +Source = "https://github.com/SiftScience/sift-python" +Changelog = "https://github.com/SiftScience/sift-python/blob/master/CHANGES.md" + +[tool.setuptools] +packages = ["sift"] + +[tool.black] +line-length = 79 + +[tool.isort] +profile = "black" +combine_as_imports = true +remove_redundant_aliases = true +line_length = 79 +skip = [ + "build", +] + +[tool.mypy] +follow_imports_for_stubs = false +disallow_untyped_defs = true +disallow_incomplete_defs = true +disallow_untyped_decorators = true +warn_redundant_casts = true +warn_unused_ignores = true +warn_unreachable = true +warn_return_any = true +warn_no_return = true +enable_error_code = "possibly-undefined,ignore-without-code" +exclude = [ + "build", +] diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000..a78b232 --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,5 @@ +black==24.8.0 +flake8==7.1.2 +isort==5.13.2 +mypy==1.14.1 +typos==1.30.2 diff --git a/setup.py b/setup.py deleted file mode 100644 index 278864e..0000000 --- a/setup.py +++ /dev/null @@ -1,71 +0,0 @@ -try: - from imp import load_source -except ImportError: - import importlib.util - import importlib.machinery - - def load_source(modname, filename): - loader = importlib.machinery.SourceFileLoader(modname, filename) - spec = importlib.util.spec_from_file_location(modname, filename, loader=loader) - module = importlib.util.module_from_spec(spec) - # The module is always executed and not cached in sys.modules. - # Uncomment the following line to cache the module. - # sys.modules[module.__name__] = module - loader.exec_module(module) - return module - - -import os - -try: - from setuptools import setup -except ImportError: - from distutils.core import setup - - -here = os.path.abspath(os.path.dirname(__file__)) - -try: - README = open(os.path.join(here, 'README.md')).read() - CHANGES = open(os.path.join(here, 'CHANGES.md')).read() -except Exception: - README = '' - CHANGES = '' - -# Use imp/importlib to avoid sift/__init__.py -version_mod = load_source('__tmp', os.path.join(here, 'sift/version.py')) - -setup( - name='Sift', - description='Python bindings for Sift Science\'s API', - version=version_mod.VERSION, - url='https://siftscience.com', - python_requires=">=2.7", - - author='Sift Science', - author_email='support@siftscience.com', - long_description_content_type="text/markdown", - long_description=README + '\n\n' + CHANGES, - - packages=['sift'], - install_requires=[ - "requests >= 0.14.1", - "six >= 1.16.0", - ], - extras_require={ - 'test': [ - 'mock >= 1.0.1', - 'unittest2 >= 1, < 2', - ], - }, - - classifiers=[ - "Programming Language :: Python", - "Programming Language :: Python :: 2.7", - "Programming Language :: Python :: 3", - "Development Status :: 4 - Beta", - "Intended Audience :: Developers", - "License :: OSI Approved :: MIT License", - "Topic :: Software Development :: Libraries :: Python Modules" - ] -) diff --git a/sift/__init__.py b/sift/__init__.py index c1b28a2..e9ad03f 100644 --- a/sift/__init__.py +++ b/sift/__init__.py @@ -1,3 +1,9 @@ -api_key = None -account_id = None +from __future__ import annotations + from .client import Client +from .version import VERSION + +__version__ = VERSION + +api_key: str | None = None +account_id: str | None = None diff --git a/sift/client.py b/sift/client.py index 87469a7..8b9c85b 100644 --- a/sift/client.py +++ b/sift/client.py @@ -1,231 +1,571 @@ """Python client for Sift Science's API. -See: https://siftscience.com/docs/references/events-api +See: https://developers.sift.com/docs/python/events-api """ -import decimal +from __future__ import annotations + import json -import requests -import requests.auth import sys +import typing as t +from collections.abc import Mapping, Sequence + +import requests +from requests.auth import HTTPBasicAuth + +import sift +from sift.constants import API_URL, DECISION_SOURCES +from sift.exceptions import ApiException +from sift.utils import DecimalEncoder, quote_path as _q +from sift.version import API_VERSION, VERSION + +AbuseType = t.Literal[ + "account_abuse", + "account_takeover", + "content_abuse", + "legacy", + "payment_abuse", + # TODO: Ask which of the following is supported (?) + "promo_abuse", + "promotion_abuse", +] + + +def _assert_non_empty_str( + val: object, + name: str, + error_cls: type[Exception] | None = None, +) -> None: + error = f"{name} must be a non-empty string" + + if not isinstance(val, str): + error_cls = error_cls or TypeError + raise error_cls(error) -if sys.version_info[0] < 3: - import six.moves.urllib as urllib + if not val: + error_cls = error_cls or ValueError + raise error_cls(error) - _UNICODE_STRING = str -else: - import urllib.parse - _UNICODE_STRING = str +def _assert_non_empty_dict(val: object, name: str) -> None: + error = f"{name} must be a non-empty dict" + + if not isinstance(val, dict): + raise TypeError(error) + + if not val: + raise ValueError(error) -import sift -import sift.version -API_URL = 'https://api.siftscience.com' -API3_URL = 'https://api3.siftscience.com' -API_URL_VERIFICATION = 'https://api.sift.com/v1/verification/' +class Response: + HTTP_CODES_WITHOUT_BODY = (204, 304) -DECISION_SOURCES = ['MANUAL_REVIEW', 'AUTOMATED_RULE', 'CHARGEBACK'] + def __init__(self, http_response: requests.Response) -> None: + """ + Raises ApiException on invalid JSON in Response body or non-2XX HTTP + status code. + """ + + self.url: str = http_response.url + self.http_status_code: int = http_response.status_code + self.api_status: int | None = None + self.api_error_message: str | None = None + self.body: dict[str, t.Any] | None = None + self.request: dict[str, t.Any] | None = None + + if ( + self.http_status_code not in self.HTTP_CODES_WITHOUT_BODY + ) and http_response.text: + try: + self.body = http_response.json() + + if "status" in self.body: + self.api_status = self.body["status"] + if "error_message" in self.body: + self.api_error_message = self.body["error_message"] + + if isinstance(self.body.get("request"), str): + self.request = json.loads(self.body["request"]) + except ValueError: + raise ApiException( + f"Failed to parse json response from {self.url}", + url=self.url, + http_status_code=self.http_status_code, + body=self.body, + api_status=self.api_status, + api_error_message=self.api_error_message, + request=self.request, + ) + finally: + if not 200 <= self.http_status_code < 300: + raise ApiException( + f"{self.url} returned non-2XX http status code {self.http_status_code}", + url=self.url, + http_status_code=self.http_status_code, + body=self.body, + api_status=self.api_status, + api_error_message=self.api_error_message, + request=self.request, + ) -def _quote_path(s): - # by default, urllib.quote doesn't escape forward slash; pass the - # optional arg to override this - return urllib.parse.quote(s, '') + def __str__(self) -> str: + body = ( + f'"body": {json.dumps(self.body)}, ' + if self.body is not None + else "" + ) + return f'{body}"http_status_code": {self.http_status_code}' -class DecimalEncoder(json.JSONEncoder): - def default(self, o): - if isinstance(o, decimal.Decimal): - return (str(o),) - return super(DecimalEncoder, self).default(o) + def is_ok(self) -> bool: + return self.api_status == 0 or self.http_status_code in (200, 204) -class Client(object): +class Client: + api_key: str + account_id: str def __init__( - self, - api_key=None, - api_url=API_URL, - timeout=2.0, - account_id=None, - version=sift.version.API_VERSION, - session=None): + self, # TODO: Require to pass all arguments as a keyword arguments (?) + api_key: str | None = None, + api_url: str = API_URL, + timeout: ( + int + | float + | tuple[int | float, int | float] + | tuple[int | float, int | float] + ) = 2, + account_id: str | None = None, # TODO: Move as a second argument (?) + version: str = API_VERSION, + session: requests.Session | None = None, + ) -> None: """Initialize the client. Args: - api_key: Your Sift Science API key associated with your customer - account. You can obtain this from - https://siftscience.com/console/developer/api-keys . + api_key: + The Sift Science API key associated with your account. You can + obtain it from https://console.sift.com/developer/api-keys - api_url: Base URL, including scheme and host, for sending events. - Defaults to 'https://api.siftscience.com'. + api_url(optional): + Base URL, including scheme and host, for sending events. + Defaults to 'https://api.sift.com'. - timeout: Number of seconds to wait before failing request. Defaults - to 2 seconds. + timeout(optional): + Number of seconds to wait before failing a request. + Defaults to 2 seconds. - account_id: The ID of your Sift Science account. You can obtain - this from https://siftscience.com/console/account/profile . + account_id(optional): + The ID of your Sift Science account. You can obtain + it from https://developers.sift.com/console/account/profile - version: The version of the Sift Science API to call. Defaults to - the latest version ('205'). + version{optional}: + The version of the Sift Science API to call. + Defaults to the latest version. + session(optional): + requests.Session object + https://requests.readthedocs.io/en/latest/user/advanced/#session-objects """ - _assert_non_empty_unicode(api_url, 'api_url') + _assert_non_empty_str(api_url, "api_url") if api_key is None: api_key = sift.api_key - _assert_non_empty_unicode(api_key, 'api_key') + _assert_non_empty_str(api_key, "api_key") self.session = session or requests.Session() - self.api_key = api_key + self.api_key = t.cast(str, api_key) self.url = api_url self.timeout = timeout - self.account_id = account_id or sift.account_id + self.account_id = t.cast(str, account_id or sift.account_id) self.version = version - def track( - self, - event, - properties, - path=None, - return_score=False, - return_action=False, - return_workflow_status=False, - return_route_info=False, - force_workflow_run=False, - abuse_types=None, - timeout=None, - version=None, - include_score_percentiles=False, - include_warnings=False): - """Track an event and associated properties to the Sift Science client. - This call is blocking. Check out https://siftscience.com/resources/references/events-api - for more information on what types of events you can send and fields you can add to the - properties parameter. + @staticmethod + def _get_fields_param( + include_score_percentiles: bool, + include_warnings: bool, + ) -> list[str]: + return [ + field + for include, field in ( + (include_score_percentiles, "SCORE_PERCENTILES"), + (include_warnings, "WARNINGS"), + ) + if include + ] - Args: - event: The name of the event to send. This can either be a reserved - event name such as "$transaction" or "$create_order" or a custom event - name (that does not start with a $). + def _auth(self) -> HTTPBasicAuth: + return HTTPBasicAuth(self.api_key, "") + + def _api_url(self, version: str, endpoint: str) -> str: + return f"{self.url}/{version}{endpoint}" + + def _versioned_api(self, version: str, endpoint: str) -> str: + return self._api_url(f"v{version}", endpoint) + + def _v1_api(self, endpoint: str) -> str: + return self._api_url("v1", endpoint) + + def _v3_api(self, endpoint: str) -> str: + return self._api_url("v3", endpoint) + + def _user_agent(self, version: str | None = None) -> str: + return ( + f"SiftScience/v{version or self.version} " + f"sift-python/{VERSION} " + f"Python/{sys.version.split(' ')[0]}" + ) + + def _headers(self, version: str | None = None) -> dict[str, str]: + return { + "User-Agent": self._user_agent(version), + } + + def _post_headers(self, version: str | None = None) -> dict[str, str]: + return { + "Content-type": "application/json", + "Accept": "*/*", + "User-Agent": self._user_agent(version), + } + + def _events_url(self, version: str) -> str: + return self._versioned_api(version, "/events") - properties: A dict of additional event-specific attributes to track. + def _score_url(self, user_id: str, version: str) -> str: + return self._versioned_api(version, f"/score/{_q(user_id)}") - return_score: Whether the API response should include a score for this - user (the score will be calculated using this event). + def _user_score_url(self, user_id: str, version: str) -> str: + return self._versioned_api(version, f"/users/{_q(user_id)}/score") - return_action: Whether the API response should include actions in the response. For - more information on how this works, please visit the tutorial at: - https://siftscience.com/resources/tutorials/formulas . + def _labels_url(self, user_id: str, version: str) -> str: + return self._versioned_api(version, f"/users/{_q(user_id)}/labels") - return_workflow_status: Whether the API response should - include the status of any workflow run as a result of - the tracked event. + def _workflow_status_url(self, account_id: str, run_id: str) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/workflows/runs/{_q(run_id)}" + ) - return_route_info: Whether to get the route information from the Workflow Decision. - This parameter must be used with the return_workflow_status query parameter. + def _decisions_url(self, account_id: str) -> str: + return self._v3_api(f"/accounts/{_q(account_id)}/decisions") - force_workflow_run: TODO:(rlong) Add after Rishabh adds documentation. + def _order_decisions_url(self, account_id: str, order_id: str) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/orders/{_q(order_id)}/decisions" + ) - abuse_types(optional): List of abuse types, specifying for which abuse types a score - should be returned (if scores were requested). If not specified, a score will - be returned for every abuse_type to which you are subscribed. + def _user_decisions_url(self, account_id: str, user_id: str) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/users/{_q(user_id)}/decisions" + ) - timeout(optional): Use a custom timeout (in seconds) for this call. + def _session_decisions_url( + self, account_id: str, user_id: str, session_id: str + ) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/users/{_q(user_id)}/sessions/{_q(session_id)}/decisions" + ) - version(optional): Use a different version of the Sift Science API for this call. + def _content_decisions_url( + self, account_id: str, user_id: str, content_id: str + ) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/users/{_q(user_id)}/content/{_q(content_id)}/decisions" + ) - include_score_percentiles(optional) : Whether to add new parameter in the query parameter. - if include_score_percentiles is true then add a new parameter called fields in the query parameter + def _order_apply_decisions_url( + self, account_id: str, user_id: str, order_id: str + ) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/users/{_q(user_id)}/orders/{_q(order_id)}/decisions" + ) - include_warnings(optional) : Whether the API response should include `warnings` field. - if include_warnings is True `warnings` field returns the amount of validation warnings - along with their descriptions. They are not critical enough to reject the whole request, + def _psp_merchant_url(self, account_id: str) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/psp_management/merchants" + ) + + def _psp_merchant_id_url(self, account_id: str, merchant_id: str) -> str: + return self._v3_api( + f"/accounts/{_q(account_id)}/psp_management/merchants/{_q(merchant_id)}" + ) + + def _verification_send_url(self) -> str: + return self._v1_api("/verification/send") + + def _verification_resend_url(self) -> str: + return self._v1_api("/verification/resend") + + def _verification_check_url(self) -> str: + return self._v1_api("/verification/check") + + def _validate_send_request(self, properties: Mapping[str, t.Any]) -> None: + """This method is used to validate arguments passed to the send method.""" + + _assert_non_empty_dict(properties, "properties") + + user_id = properties.get("$user_id") + _assert_non_empty_str(user_id, "user_id", error_cls=ValueError) + + send_to = properties.get("$send_to") + _assert_non_empty_str(send_to, "send_to", error_cls=ValueError) + + verification_type = properties.get("$verification_type") + _assert_non_empty_str( + verification_type, "verification_type", error_cls=ValueError + ) + + event = properties.get("$event") + if not isinstance(event, dict): + raise TypeError("$event must be a dict") + elif not event: + raise ValueError("$event dictionary may not be empty") + + session_id = event.get("$session_id") + _assert_non_empty_str(session_id, "session_id", error_cls=ValueError) + + def _validate_resend_request( + self, + properties: Mapping[str, t.Any], + ) -> None: + """This method is used to validate arguments passed to the send method.""" + + _assert_non_empty_dict(properties, "properties") + + user_id = properties.get("$user_id") + _assert_non_empty_str(user_id, "user_id", error_cls=ValueError) + + def _validate_check_request(self, properties: Mapping[str, t.Any]) -> None: + """This method is used to validate arguments passed to the check method.""" + + _assert_non_empty_dict(properties, "properties") + + user_id = properties.get("$user_id") + _assert_non_empty_str(user_id, "user_id", error_cls=ValueError) + + otp_code = properties.get("$code") + if otp_code is None: + raise ValueError("code is required") + + def _validate_apply_decision_request( + self, + properties: Mapping[str, t.Any], + user_id: str, + ) -> None: + _assert_non_empty_str(user_id, "user_id") + _assert_non_empty_dict(properties, "properties") + + source = properties.get("source") + + _assert_non_empty_str(source, "source", error_cls=ValueError) + + if source not in DECISION_SOURCES: + raise ValueError( + f"decision 'source' must be one of {list(DECISION_SOURCES)}" + ) + + if source == "MANUAL_REVIEW" and not properties.get("analyst"): + raise ValueError( + "must provide 'analyst' for decision 'source': 'MANUAL_REVIEW'" + ) + + def track( + self, + event: str, + properties: Mapping[str, t.Any], + path: str | None = None, + return_score: bool = False, + return_action: bool = False, + return_workflow_status: bool = False, + return_route_info: bool = False, + force_workflow_run: bool = False, + abuse_types: Sequence[AbuseType] | None = None, + timeout: int | float | tuple[int | float, int | float] | None = None, + version: str | None = None, + include_score_percentiles: bool = False, + include_warnings: bool = False, + ) -> Response: + """ + Track an event and associated properties to the Sift Science client. + + This call is blocking. + + Visit https://siftscience.com/resources/references/events-api + for more information on what types of events you can send and fields + you can add to the properties parameter. + + Args: + event: + The name of the event to send. This can either be a reserved + event name such as "$transaction" or "$create_order" or + a custom event name (that does not start with a $). + + properties: + A dict of additional event-specific attributes to track. + + path: + An API endpoint to make a request to. + Defaults to Events API Endpoint + + return_score (optional): + Whether the API response should include a score for + this user (the score will be calculated using this event). + + return_action (optional): + Whether the API response should include actions in the + response. For more information on how this works, please + visit the tutorial at: + https://developers.sift.com/tutorials/formulas . + + return_workflow_status (optional): + Whether the API response should include the status of any + workflow run as a result of the tracked event. + + return_route_info (optional): + Whether to get the route information from the Workflow + Decision. This parameter must be used with the + `return_workflow_status` query parameter. + + force_workflow_run (optional): + Set to True to run the Workflow Asynchronously if your Workflow + is set to only run on API Request. If a Workflow is not running + on the event you send this with, there will be no error or + score response, and no workflow will run. + + abuse_types (optional): + A Sequence of abuse types, specifying for which abuse types + a score should be returned (if scores were requested). If not + specified, a score will be returned for every abuse_type + to which you are subscribed. + + timeout (optional): + Use a custom timeout (in seconds) for this call. + + version (optional): + Use a different version of the Sift Science API for this call. + + include_score_percentiles (optional): + Whether to add new parameter in the query parameter. if + `include_score_percentiles` is True then add a new parameter + called fields in the query parameter + + include_warnings (optional): + Whether the API response should include `warnings` field. + If `include_warnings` is True `warnings` field returns the + amount of validation warnings along with their descriptions. + They are not critical enough to reject the whole request, but important enough to be fixed. + Returns: - A sift.client.Response object if the track call succeeded, otherwise - raises an ApiException. + A sift.client.Response object if the call succeeded + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(event, 'event') - _assert_non_empty_dict(properties, 'properties') - - headers = {'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()} + _assert_non_empty_str(event, "event") + _assert_non_empty_dict(properties, "properties") if version is None: version = self.version if path is None: - path = self._event_url(version) + path = self._events_url(version) if timeout is None: timeout = self.timeout - properties.update({'$api_key': self.api_key, '$type': event}) - params = {} + _properties = { + **properties, + "$api_key": self.api_key, + "$type": event, + } + + params: dict[str, t.Any] = {} if return_score: - params['return_score'] = 'true' + params["return_score"] = "true" if return_action: - params['return_action'] = 'true' + params["return_action"] = "true" if abuse_types: - params['abuse_types'] = ','.join(abuse_types) + params["abuse_types"] = ",".join(abuse_types) if return_workflow_status: - params['return_workflow_status'] = 'true' + params["return_workflow_status"] = "true" if return_route_info: - params['return_route_info'] = 'true' + params["return_route_info"] = "true" if force_workflow_run: - params['force_workflow_run'] = 'true' + params["force_workflow_run"] = "true" - include_fields = Client._get_fields_param(include_score_percentiles, - include_warnings) - if include_fields: - params['fields'] = ",".join(include_fields) + include_fields = self._get_fields_param( + include_score_percentiles, include_warnings + ) + if include_fields: + params["fields"] = ",".join(include_fields) try: response = self.session.post( path, - data=json.dumps(properties, cls=DecimalEncoder), - headers=headers, + data=json.dumps(_properties, cls=DecimalEncoder), + headers=self._post_headers(version), timeout=timeout, - params=params) - return Response(response) + params=params, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), path) - def score(self, user_id, timeout=None, abuse_types=None, version=None, include_score_percentiles=False): - """Retrieves a user's fraud score from the Sift Science API. - This call is blocking. Check out https://siftscience.com/resources/references/score_api.html - for more information on our Score response structure. + return Response(response) + + def score( + self, + user_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + abuse_types: Sequence[AbuseType] | None = None, + version: str | None = None, + include_score_percentiles: bool = False, + ) -> Response: + """ + Retrieves a user's fraud score from the Sift Science API. + + This call is blocking. + + Visit https://developers.sift.com/docs/python/score-api + for more details on our Score response structure. Args: - user_id: A user's id. This id should be the same as the user_id used in - event calls. + user_id: + A user's id. This id should be the same as the `user_id` + used in event calls. - timeout(optional): Use a custom timeout (in seconds) for this call. + timeout (optional): + Use a custom timeout (in seconds) for this call. - abuse_types(optional): List of abuse types, specifying for which abuse types a score - should be returned (if scores were requested). If not specified, a score will - be returned for every abuse_type to which you are subscribed. + abuse_types (optional): + A Sequence of abuse types, specifying for which abuse types + a score should be returned (if scores were requested). If not + specified, a score will be returned for every abuse_type + to which you are subscribed. - version(optional): Use a different version of the Sift Science API for this call. + version (optional): + Use a different version of the Sift Science API for this call. - include_score_percentiles(optional) : Whether to add new parameter in the query parameter. - if include_score_percentiles is true then add a new parameter called fields in the query parameter + include_score_percentiles (optional): + Whether to add new parameter in the query parameter. + if `include_score_percentiles` is True then add a new + parameter called `fields` in the query parameter Returns: - A sift.client.Response object if the score call succeeded, or raises - an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(user_id, 'user_id') + _assert_non_empty_str(user_id, "user_id") if timeout is None: timeout = self.timeout @@ -233,168 +573,248 @@ def score(self, user_id, timeout=None, abuse_types=None, version=None, include_s if version is None: version = self.version - headers = {'User-Agent': self._user_agent()} - params = {} + params: dict[str, t.Any] = {} + if abuse_types: - params['abuse_types'] = ','.join(abuse_types) + params["abuse_types"] = ",".join(abuse_types) if include_score_percentiles: - params['fields'] = 'SCORE_PERCENTILES' + params["fields"] = "SCORE_PERCENTILES" url = self._score_url(user_id, version) try: response = self.session.get( url, - headers=headers, - timeout=timeout, params=params, - auth=requests.auth.HTTPBasicAuth(self.api_key, '')) - return Response(response) + auth=self._auth(), + headers=self._headers(version), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_user_score(self, user_id, timeout=None, abuse_types=None, include_score_percentiles=False): - """Fetches the latest score(s) computed for the specified user and abuse types from the Sift Science API. - As opposed to client.score() and client.rescore_user(), this *does not* compute a new score for the user; it - simply fetches the latest score(s) which have computed. These scores may be arbitrarily old. + return Response(response) + + def get_user_score( + self, + user_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + abuse_types: Sequence[AbuseType] | None = None, + include_score_percentiles: bool = False, + ) -> Response: + """ + Fetches the latest score(s) computed for the specified user and + abuse types from the Sift Science API. As opposed to client.score() + and client.rescore_user(), this *does not* compute a new score for + the user; it simply fetches the latest score(s) which have computed. + These scores may be arbitrarily old. + + This call is blocking. - This call is blocking. See https://siftscience.com/developers/docs/python/score-api/get-score for more details. + Visit https://developers.sift.com/docs/python/score-api/get-score + for more details. Args: - user_id: A user's id. This id should be the same as the user_id used in + user_id: + A user's id. This id should be the same as the user_id used in event calls. - timeout(optional): Use a custom timeout (in seconds) for this call. + timeout (optional): + Use a custom timeout (in seconds) for this call. - abuse_types(optional): List of abuse types, specifying for which abuse types a score - should be returned (if scores were requested). If not specified, a score will - be returned for every abuse_type to which you are subscribed. + abuse_types (optional): + A Sequence of abuse types, specifying for which abuse types + a score should be returned (if scores were requested). If not + specified, a score will be returned for every abuse_type + to which you are subscribed. - include_score_percentiles(optional) : Whether to add new parameter in the query parameter. - if include_score_percentiles is true then add a new parameter called fields in the query parameter + include_score_percentiles (optional): + Whether to add new parameter in the query parameter. + if include_score_percentiles is True then add a new parameter + called fields in the query parameter Returns: - A sift.client.Response object if the score call succeeded, or raises - an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(user_id, 'user_id') + _assert_non_empty_str(user_id, "user_id") if timeout is None: timeout = self.timeout url = self._user_score_url(user_id, self.version) - headers = {'User-Agent': self._user_agent()} - params = {} + params: dict[str, t.Any] = {} + if abuse_types: - params['abuse_types'] = ','.join(abuse_types) + params["abuse_types"] = ",".join(abuse_types) if include_score_percentiles: - params['fields'] = 'SCORE_PERCENTILES' + params["fields"] = "SCORE_PERCENTILES" try: response = self.session.get( url, - headers=headers, - timeout=timeout, params=params, - auth=requests.auth.HTTPBasicAuth(self.api_key, '')) - return Response(response) + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def rescore_user(self, user_id, timeout=None, abuse_types=None): - """Rescores the specified user for the specified abuse types and returns the resulting score(s). - This call is blocking. See https://siftscience.com/developers/docs/python/score-api/rescore for more details. + return Response(response) + + def rescore_user( + self, + user_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + abuse_types: Sequence[AbuseType] | None = None, + ) -> Response: + """ + Rescores the specified user for the specified abuse types and returns + the resulting score(s). + + This call is blocking. + + Visit https://developers.sift.com/docs/python/score-api/rescore/overview + for more details. Args: - user_id: A user's id. This id should be the same as the user_id used in + user_id: + A user's id. This id should be the same as the user_id used in event calls. - timeout(optional): Use a custom timeout (in seconds) for this call. + timeout (optional): + Use a custom timeout (in seconds) for this call. - abuse_types(optional): List of abuse types, specifying for which abuse types a score - should be returned (if scores were requested). If not specified, a score will - be returned for every abuse_type to which you are subscribed. + abuse_types (optional): + A Sequence of abuse types, specifying for which abuse types + a score should be returned (if scores were requested). If not + specified, a score will be returned for every abuse_type + to which you are subscribed. Returns: - A sift.client.Response object if the score call succeeded, or raises - an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(user_id, 'user_id') + _assert_non_empty_str(user_id, "user_id") if timeout is None: timeout = self.timeout url = self._user_score_url(user_id, self.version) - headers = {'User-Agent': self._user_agent()} - params = {} + params: dict[str, t.Any] = {} + if abuse_types: - params['abuse_types'] = ','.join(abuse_types) + params["abuse_types"] = ",".join(abuse_types) try: response = self.session.post( url, - headers=headers, - timeout=timeout, params=params, - auth=requests.auth.HTTPBasicAuth(self.api_key, '')) - return Response(response) + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def label(self, user_id, properties, timeout=None, version=None): - """Labels a user as either good or bad through the Sift Science API. - This call is blocking. Check out https://siftscience.com/resources/references/labels_api.html - for more information on what fields to send in properties. + return Response(response) + + def label( + self, + user_id: str, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + version: str | None = None, + ) -> Response: + """ + Labels a user as either good or bad through the Sift Science API. + + This call is blocking. + + Visit https://developers.sift.com/docs/python/labels-api + for more details on what fields to send in properties. Args: - user_id: A user's id. This id should be the same as the user_id used in + user_id: + A user's id. This id should be the same as the user_id used in event calls. - properties: A dict of additional event-specific attributes to track. + properties: + A dict of additional event-specific attributes to track. - timeout(optional): Use a custom timeout (in seconds) for this call. + timeout (optional): + Use a custom timeout (in seconds) for this call. - version(optional): Use a different version of the Sift Science API for this call. + version (optional): + Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the label call succeeded, otherwise - raises an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(user_id, 'user_id') + _assert_non_empty_str(user_id, "user_id") if version is None: version = self.version return self.track( - '$label', + "$label", properties, - path=self._label_url(user_id, version), + path=self._labels_url(user_id, version), timeout=timeout, - version=version) + version=version, + ) + + def unlabel( + self, + user_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + abuse_type: AbuseType | None = None, + version: str | None = None, + ) -> Response: + """ + Unlabels a user through the Sift Science API. - def unlabel(self, user_id, timeout=None, abuse_type=None, version=None): - """unlabels a user through the Sift Science API. - This call is blocking. Check out https://siftscience.com/resources/references/labels_api.html - for more information. + This call is blocking. + + Visit https://developers.sift.com/docs/python/labels-api + for more details. Args: - user_id: A user's id. This id should be the same as the user_id used in + user_id: + A user's id. This id should be the same as the user_id used in event calls. - timeout(optional): Use a custom timeout (in seconds) for this call. + timeout (optional): + Use a custom timeout (in seconds) for this call. - abuse_type(optional): The abuse type for which the user should be unlabeled. + abuse_type (optional): + The abuse type for which the user should be unlabeled. If omitted, the user is unlabeled for all abuse types. - version(optional): Use a different version of the Sift Science API for this call. + version (optional): + Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the unlabel call succeeded, otherwise - raises an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(user_id, 'user_id') + _assert_non_empty_str(user_id, "user_id") if timeout is None: timeout = self.timeout @@ -402,196 +822,276 @@ def unlabel(self, user_id, timeout=None, abuse_type=None, version=None): if version is None: version = self.version - url = self._label_url(user_id, version) - headers = {'User-Agent': self._user_agent()} - params = {} + url = self._labels_url(user_id, version) + params: dict[str, t.Any] = {} + if abuse_type: - params['abuse_type'] = abuse_type + params["abuse_type"] = abuse_type try: response = self.session.delete( url, - headers=headers, - timeout=timeout, params=params, - auth=requests.auth.HTTPBasicAuth(self.api_key, '')) - return Response(response) - + auth=self._auth(), + headers=self._headers(version), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_workflow_status(self, run_id, timeout=None): + return Response(response) + + def get_workflow_status( + self, + run_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Gets the status of a workflow run. Args: - run_id: The ID of a workflow run. + run_id: + The workflow run unique identifier. + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object if the call succeeded. - Otherwise, raises an ApiException. + A sift.client.Response object if the call succeeded + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(run_id, 'run_id') + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(run_id, "run_id") url = self._workflow_status_url(self.account_id, run_id) + if timeout is None: timeout = self.timeout try: - return Response(self.session.get( + response = self.session.get( url, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_decisions(self, entity_type, limit=None, start_from=None, abuse_types=None, timeout=None): - """Get decisions available to customer + return Response(response) + + def get_decisions( + self, + entity_type: t.Literal["user", "order", "session", "content"], + limit: int | None = None, + start_from: int | None = None, + abuse_types: ( + str | None + ) = None, # TODO: Ask if here should be a Sequence[AbuseType] instead of str + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: + """Get decisions available to the customer Args: - entity_type: only return decisions applicable to entity type {USER|ORDER|SESSION|CONTENT} - limit: number of query results (decisions) to return [optional, default: 100] - start_from: result set offset for use in pagination [optional, default: 0] - abuse_types: comma-separated list of abuse_types used to filter returned decisions (optional) + entity_type: + Return decisions applicable to entity type + One of: "user", "order", "session", "content" + + limit (optional): + Number of query results (decisions) to return [default: 100] + + start_from (optional): + Result set offset for use in pagination [default: 0] + + abuse_types (optional): + comma-separated list of abuse_types used to filter returned + decisions + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object containing array of decisions if call succeeded - Otherwise raises an ApiException - """ + A sift.client.Response object if the call succeeded - if timeout is None: - timeout = self.timeout + Raises: + ApiException: + if the call not succeeded + """ - params = {} + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(entity_type, "entity_type") - _assert_non_empty_unicode(entity_type, 'entity_type') - if entity_type.lower() not in ['user', 'order', 'session', 'content']: - raise ValueError("entity_type must be one of {user, order, session, content}") + if entity_type.lower() not in ["user", "order", "session", "content"]: + raise ValueError( + "entity_type must be one of {user, order, session, content}" + ) - params['entity_type'] = entity_type + params: dict[str, t.Any] = { + "entity_type": entity_type, + } if limit: - params['limit'] = limit + params["limit"] = limit if start_from: - params['from'] = start_from + params["from"] = start_from if abuse_types: - params['abuse_types'] = abuse_types + params["abuse_types"] = abuse_types - url = self._get_decisions_url(self.account_id) + if timeout is None: + timeout = self.timeout - try: - return Response(self.session.get(url, params=params, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, timeout=timeout)) + url = self._decisions_url(self.account_id) + try: + response = self.session.get( + url, + params=params, + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def apply_user_decision(self, user_id, properties, timeout=None): - """Apply decision to user + return Response(response) + + def apply_user_decision( + self, + user_id: str, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: + """Apply decision to a user Args: - user_id: id of user + user_id: id of a user + properties: - decision_id: decision to apply to user + decision_id: decision to apply to a user source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK} analyst: id or email, required if 'source: MANUAL_REVIEW' time: in millis when decision was applied - Returns - A sift.client.Response object if the call succeeded, else raises an ApiException + + timeout (optional): + Use a custom timeout (in seconds) for this call. + + Returns: + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ + _assert_non_empty_str(self.account_id, "account_id") + + self._validate_apply_decision_request(properties, user_id) if timeout is None: timeout = self.timeout - self._validate_apply_decision_request(properties, user_id) url = self._user_decisions_url(self.account_id, user_id) + try: - return Response(self.session.post( + response = self.session.post( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def apply_order_decision(self, user_id, order_id, properties, timeout=None): + return Response(response) + + def apply_order_decision( + self, + user_id: str, + order_id: str, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Apply decision to order Args: - user_id: id of user - order_id: id of order + user_id: + ID of a user. + + order_id: + The ID for the order. + properties: decision_id: decision to apply to order source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK} analyst: id or email, required if 'source: MANUAL_REVIEW' description: free form text (optional) time: in millis when decision was applied (optional) - Returns - A sift.client.Response object if the call succeeded, else raises an ApiException + + timeout (optional): + Use a custom timeout (in seconds) for this call. + + Returns: + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(user_id, "user_id") + _assert_non_empty_str(order_id, "order_id") + + self._validate_apply_decision_request(properties, user_id) + if timeout is None: timeout = self.timeout - _assert_non_empty_unicode(user_id, 'user_id') - _assert_non_empty_unicode(order_id, 'order_id') - - self._validate_apply_decision_request(properties, user_id) + url = self._order_apply_decisions_url( + self.account_id, user_id, order_id + ) - url = self._order_apply_decisions_url(self.account_id, user_id, order_id) try: - return Response(self.session.post( + response = self.session.post( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def _validate_apply_decision_request(self, properties, user_id): - _assert_non_empty_unicode(user_id, 'user_id') - - if not isinstance(properties, dict): - raise TypeError("properties must be a dict") - elif not properties: - raise ValueError("properties dictionary may not be empty") - - source = properties.get('source') - - _assert_non_empty_unicode(source, 'source', error_cls=ValueError) - if source not in DECISION_SOURCES: - raise ValueError("decision 'source' must be one of [{0}]".format(", ".join(DECISION_SOURCES))) - - properties.update({'source': source.upper()}) - - if source == 'MANUAL_REVIEW' and not properties.get('analyst', None): - raise ValueError("must provide 'analyst' for decision 'source': 'MANUAL_REVIEW'") + return Response(response) - def get_user_decisions(self, user_id, timeout=None): + def get_user_decisions( + self, + user_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Gets the decisions for a user. Args: - user_id: The ID of a user. + user_id: + The ID of a user. + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object if the call succeeded. - Otherwise, raises an ApiException. + A sift.client.Response object if the call succeeded + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(user_id, 'user_id') + + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(user_id, "user_id") if timeout is None: timeout = self.timeout @@ -599,27 +1099,41 @@ def get_user_decisions(self, user_id, timeout=None): url = self._user_decisions_url(self.account_id, user_id) try: - return Response(self.session.get( + response = self.session.get( url, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_order_decisions(self, order_id, timeout=None): + return Response(response) + + def get_order_decisions( + self, + order_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Gets the decisions for an order. Args: - order_id: The ID of an order. + order_id: + The ID for the order. + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object if the call succeeded. - Otherwise, raises an ApiException. + A sift.client.Response object if the call succeeded + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(order_id, 'order_id') + + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(order_id, "order_id") if timeout is None: timeout = self.timeout @@ -627,29 +1141,46 @@ def get_order_decisions(self, order_id, timeout=None): url = self._order_decisions_url(self.account_id, order_id) try: - return Response(self.session.get( + response = self.session.get( url, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_content_decisions(self, user_id, content_id, timeout=None): + return Response(response) + + def get_content_decisions( + self, + user_id: str, + content_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Gets the decisions for a piece of content. Args: - user_id: The ID of the owner of the content. - content_id: The ID of a piece of content. + user_id: + The ID of the owner of the content. + + content_id: + The ID for the content. + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object if the call succeeded. - Otherwise, raises an ApiException. + A sift.client.Response object if the call succeeded + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(content_id, 'content_id') - _assert_non_empty_unicode(user_id, 'user_id') + + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(content_id, "content_id") + _assert_non_empty_str(user_id, "user_id") if timeout is None: timeout = self.timeout @@ -657,29 +1188,46 @@ def get_content_decisions(self, user_id, content_id, timeout=None): url = self._content_decisions_url(self.account_id, user_id, content_id) try: - return Response(self.session.get( + response = self.session.get( url, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_session_decisions(self, user_id, session_id, timeout=None): + return Response(response) + + def get_session_decisions( + self, + user_id: str, + session_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Gets the decisions for a user's session. Args: - user_id: The ID of a user. - session_id: The ID of a session. + user_id: + The ID for the user. + + session_id: + The ID for the session. + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object if the call succeeded. - Otherwise, raises an ApiException. + A sift.client.Response object if the call succeeded + Raises: + ApiException: + if the call not succeeded """ - _assert_non_empty_unicode(user_id, 'user_id') - _assert_non_empty_unicode(session_id, 'session_id') + + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(user_id, "user_id") + _assert_non_empty_str(session_id, "session_id") if timeout is None: timeout = self.timeout @@ -687,552 +1235,528 @@ def get_session_decisions(self, user_id, session_id, timeout=None): url = self._session_decisions_url(self.account_id, user_id, session_id) try: - return Response(self.session.get( + response = self.session.get( url, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def apply_session_decision(self, user_id, session_id, properties, timeout=None): - """Apply decision to session + return Response(response) + + def apply_session_decision( + self, + user_id: str, + session_id: str, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: + """Apply decision to a session. Args: - user_id: id of user - session_id: id of session + user_id: + The ID for the user. + + session_id: + The ID for the session. + properties: decision_id: decision to apply to session source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK} analyst: id or email, required if 'source: MANUAL_REVIEW' description: free form text (optional) time: in millis when decision was applied (optional) - Returns - A sift.client.Response object if the call succeeded, else raises an ApiException - """ - if timeout is None: - timeout = self.timeout + timeout (optional): + Use a custom timeout (in seconds) for this call. + + Returns: + A sift.client.Response object if the call succeeded - _assert_non_empty_unicode(session_id, 'session_id') + Raises: + ApiException: + if the call not succeeded + """ + + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(user_id, "user_id") + _assert_non_empty_str(session_id, "session_id") self._validate_apply_decision_request(properties, user_id) - url = self._session_apply_decisions_url(self.account_id, user_id, session_id) + if timeout is None: + timeout = self.timeout + + url = self._session_decisions_url(self.account_id, user_id, session_id) try: - return Response(self.session.post( + response = self.session.post( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def apply_content_decision(self, user_id, content_id, properties, timeout=None): - """Apply decision to content + return Response(response) + + def apply_content_decision( + self, + user_id: str, + content_id: str, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: + """Apply decision to a piece of content. Args: - user_id: id of user - content_id: id of content + user_id: + The ID for the user. + + content_id: + The ID for the content. + properties: decision_id: decision to apply to session source: {one of MANUAL_REVIEW | AUTOMATED_RULE | CHARGEBACK} analyst: id or email, required if 'source: MANUAL_REVIEW' description: free form text (optional) time: in millis when decision was applied (optional) - Returns - A sift.client.Response object if the call succeeded, else raises an ApiException - """ - if timeout is None: - timeout = self.timeout + timeout (optional): + Use a custom timeout (in seconds) for this call. - _assert_non_empty_unicode(content_id, 'content_id') + Returns: + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded + """ + _assert_non_empty_str(self.account_id, "account_id") + _assert_non_empty_str(user_id, "user_id") + _assert_non_empty_str(content_id, "content_id") self._validate_apply_decision_request(properties, user_id) - url = self._content_apply_decisions_url(self.account_id, user_id, content_id) + if timeout is None: + timeout = self.timeout + + url = self._content_decisions_url(self.account_id, user_id, content_id) try: - return Response(self.session.post( + response = self.session.post( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def create_psp_merchant_profile(self, properties, timeout=None): + return Response(response) + + def create_psp_merchant_profile( + self, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Create a new PSP Merchant profile + Args: - properties: A dict of merchant profile data. - Returns - A sift.client.Response object if the call succeeded, else raises an ApiException + properties: + A dict of merchant profile data. + + timeout (optional): + Use a custom timeout (in seconds) for this call. + + Returns: + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ + _assert_non_empty_str(self.account_id, "account_id") + if timeout is None: timeout = self.timeout url = self._psp_merchant_url(self.account_id) try: - return Response(self.session.post( + response = self.session.post( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def update_psp_merchant_profile(self, merchant_id, properties, timeout=None): + return Response(response) + + def update_psp_merchant_profile( + self, + merchant_id: str, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: """Update already existing PSP Merchant profile + Args: - merchant_id: id of merchant - properties: A dict of merchant profile data. - Returns - A sift.client.Response object if the call succeeded, else raises an ApiException + merchant_id: + The internal identifier for the merchant or seller providing + the good or service. + + properties: + A dict of merchant profile data. + + timeout (optional): + Use a custom timeout (in seconds) for this call. + + Returns: + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ + _assert_non_empty_str(self.account_id, "account_id") + if timeout is None: timeout = self.timeout url = self._psp_merchant_id_url(self.account_id, merchant_id) + try: - return Response(self.session.put( + response = self.session.put( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_psp_merchant_profiles(self, batch_token=None, batch_size=None, timeout=None): - """Gets all PSP merchant profiles. + return Response(response) + + def get_psp_merchant_profiles( + self, + batch_token: str | None = None, + batch_size: int | None = None, + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: + """Gets all PSP merchant profiles (paginated). + + Args: + batch_token (optional): + Batch or page position of the paginated sequence. + + batch_size: (optional): + Batch or page size of the paginated sequence. + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object if the call succeeded. - Otherwise, raises an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ + _assert_non_empty_str(self.account_id, "account_id") + if timeout is None: timeout = self.timeout url = self._psp_merchant_url(self.account_id) - params = {} + + params: dict[str, t.Any] = {} if batch_size: - params['batch_size'] = batch_size + params["batch_size"] = batch_size if batch_token: - params['batch_token'] = batch_token + params["batch_token"] = batch_token + try: - return Response(self.session.get( + response = self.session.get( url, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, + auth=self._auth(), + headers=self._headers(), params=params, - timeout=timeout)) - + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def get_a_psp_merchant_profile(self, merchant_id, timeout=None): - """Gets a PSP merchant profile using merchant id. + return Response(response) + + def get_a_psp_merchant_profile( + self, + merchant_id: str, + timeout: int | float | tuple[int | float, int | float] | None = None, + ) -> Response: + """Gets a PSP merchant profile by merchant id. + + Args: + merchant_id: + The internal identifier for the merchant or seller providing + the good or service. + + timeout (optional): + Use a custom timeout (in seconds) for this call. Returns: - A sift.client.Response object if the call succeeded. - Otherwise, raises an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ + _assert_non_empty_str(self.account_id, "account_id") + if timeout is None: timeout = self.timeout url = self._psp_merchant_id_url(self.account_id, merchant_id) try: - return Response(self.session.get( + response = self.session.get( url, - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'User-Agent': self._user_agent()}, - timeout=timeout)) + auth=self._auth(), + headers=self._headers(), + timeout=timeout, + ) except requests.exceptions.RequestException as e: raise ApiException(str(e), url) - def verification_send(self, properties, timeout=None, version=None): - """The send call triggers the generation of a OTP code that is stored by Sift and email/sms the code to the user. - This call is blocking. Check out https://sift.com/developers/docs/python/verification-api/send - for more information on our send response structure. + return Response(response) + + def verification_send( + self, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + version: str | None = None, + ) -> Response: + """ + The send call triggers the generation of an OTP code that is stored + by Sift and email/sms the code to the user. + + This call is blocking. + + Visit https://developers.sift.com/docs/python/verification-api/send + for more details on our send response structure. Args: + properties: - properties: - - $user_id: User ID of user being verified, e.g. johndoe123. - $send_to: The phone / email to send the OTP to. - $verification_type: The channel used for verification. Should be either $email or $sms. - $brand_name(optional): Name of the brand of product or service the user interacts with. - $language(optional): Language of the content of the web site. - $site_country(optional): Country of the content of the site. - $event: - $session_id: The session being verified. See $verification in the Sift Events API documentation. - $verified_event: The type of the reserved event being verified. - $reason(optional): The trigger for the verification. See $verification in the Sift Events API documentation. - $ip(optional): The user's IP address. + $user_id: + User ID of user being verified, e.g. johndoe123. + $send_to: + The phone / email to send the OTP to. + $verification_type: + The channel used for verification. Should be either $email + or $sms. + $brand_name (optional): + Name of the brand of product or service the user interacts + with. + $language (optional): + Language of the content of the web site. + $site_country (optional): + Country of the content of the site. + $event: + $session_id: + The session being verified. See $verification in the + Sift Events API documentation. + $verified_event: + The type of the reserved event being verified. + $reason (optional): + The trigger for the verification. See $verification + in the Sift Events API documentation. + $ip (optional): + The user's IP address. $browser: - $user_agent: The user agent of the browser that is verifying. Represented by the $browser object. - Use this field if the client is a browser. - + $user_agent: + The user agent of the browser that is verifying. + Represented by the $browser object. + Use this field if the client is a browser. - timeout(optional): Use a custom timeout (in seconds) for this call. + timeout (optional): + Use a custom timeout (in seconds) for this call. - version(optional): Use a different version of the Sift Science API for this call. + version (optional): + Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the send call succeeded, or raises an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ if timeout is None: timeout = self.timeout + if version is None: + version = self.version + self._validate_send_request(properties) url = self._verification_send_url() try: - return Response(self.session.post( + response = self.session.post( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(version), + timeout=timeout, + ) except requests.exceptions.RequestException as e: - raise ApiException(str(e), url) - - def _validate_send_request(self, properties): - """ This method is used to validate arguments passed to the send method. """ - - if not isinstance(properties, dict): - raise TypeError("properties must be a dict") - elif not properties: - raise ValueError("properties dictionary may not be empty") - - user_id = properties.get('$user_id') - _assert_non_empty_unicode(user_id, 'user_id', error_cls=ValueError) + raise ApiException(str(e), url) - send_to = properties.get('$send_to') - _assert_non_empty_unicode(send_to, 'send_to', error_cls=ValueError) + return Response(response) - verification_type = properties.get('$verification_type') - _assert_non_empty_unicode( - verification_type, 'verification_type', error_cls=ValueError) + def verification_resend( + self, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + version: str | None = None, + ) -> Response: + """ + A user can ask for a new OTP (one-time password) if they haven't + received the previous one, or in case the previous OTP expired. - event = properties.get('$event') - if not isinstance(event, dict): - raise TypeError("$event must be a dict") - elif not event: - raise ValueError("$event dictionary may not be empty") + This call is blocking. - session_id = event.get('$session_id') - _assert_non_empty_unicode( - session_id, 'session_id', error_cls=ValueError) - - def verification_resend(self, properties, timeout=None, version=None): - """A user can ask for a new OTP (one-time password) if they haven't received the previous one, - or in case the previous OTP expired. - This call is blocking. Check out https://sift.com/developers/docs/python/verification-api/resend + Visit https://developers.sift.com/docs/python/verification-api/resend for more information on our send response structure. Args: - properties: + properties: - $user_id: User ID of user being verified, e.g. johndoe123. - $verified_event(optional): This will be the event type that triggered the verification. - $verified_entity_id(optional): The ID of the entity impacted by the event being verified. + $user_id: + User ID of user being verified, e.g. johndoe123. + $verified_event (optional): + This will be the event type that triggered the verification. + $verified_entity_id (optional): + The ID of the entity impacted by the event being verified. - timeout(optional): Use a custom timeout (in seconds) for this call. + timeout (optional): + Use a custom timeout (in seconds) for this call. - version(optional): Use a different version of the Sift Science API for this call. + version (optional): + Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the send call succeeded, or raises an ApiException. + A sift.client.Response object if the call succeeded + + Raises: + ApiException: + if the call not succeeded """ if timeout is None: timeout = self.timeout + if version is None: + version = self.version + self._validate_resend_request(properties) url = self._verification_resend_url() try: - return Response(self.session.post( + response = self.session.post( url, data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - + auth=self._auth(), + headers=self._post_headers(version), + timeout=timeout, + ) except requests.exceptions.RequestException as e: - raise ApiException(str(e), url) - - def _validate_resend_request(self, properties): - """ This method is used to validate arguments passed to the send method. """ - - if not isinstance(properties, dict): - raise TypeError("properties must be a dict") - elif not properties: - raise ValueError("properties dictionary may not be empty") - - user_id = properties.get('$user_id') - _assert_non_empty_unicode(user_id, 'user_id', error_cls=ValueError) - - def verification_check(self, properties, timeout=None, version=None): - """The verification_check call is used for checking the OTP provided by the end user to Sift. - Sift then compares the OTP, checks rate limits and responds with a decision whether the user should be able to proceed or not. - This call is blocking. Check out https://sift.com/developers/docs/python/verification-api/check - for more information on our check response structure. - - Args: - - properties: - $user_id: User ID of user being verified, e.g. johndoe123. - $code: The code the user sent to the customer for validation.. - $verified_event(optional): This will be the event type that triggered the verification. - $verified_entity_id(optional): The ID of the entity impacted by the event being verified. - - timeout(optional): Use a custom timeout (in seconds) for this call. - version(optional): Use a different version of the Sift Science API for this call. - - Returns: - A sift.client.Response object if the check call succeeded, or raises - an ApiException. - """ - if timeout is None: - timeout = self.timeout - - self._validate_check_request(properties) - - url = self._verification_check_url() - - try: - return Response(self.session.post( - url, - data=json.dumps(properties), - auth=requests.auth.HTTPBasicAuth(self.api_key, ''), - headers={'Content-type': 'application/json', - 'Accept': '*/*', - 'User-Agent': self._user_agent()}, - timeout=timeout)) - - except requests.exceptions.RequestException as e: - raise ApiException(str(e), url) - - def _validate_check_request(self, properties): - """ This method is used to validate arguments passed to the check method. """ - - if not isinstance(properties, dict): - raise TypeError("properties must be a dict") - elif not properties: - raise ValueError("properties dictionary may not be empty") - - user_id = properties.get('$user_id') - _assert_non_empty_unicode(user_id, 'user_id', error_cls=ValueError) - - otp_code = properties.get('$code') - if otp_code is None: - raise ValueError("code is required") - - def _user_agent(self): - return 'SiftScience/v%s sift-python/%s' % (sift.version.API_VERSION, sift.version.VERSION) - - def _event_url(self, version): - return self.url + '/v%s/events' % version - - def _score_url(self, user_id, version): - return self.url + '/v%s/score/%s' % (version, _quote_path(user_id)) - - def _user_score_url(self, user_id, version): - return self.url + '/v%s/users/%s/score' % (version, urllib.parse.quote(user_id)) - - def _label_url(self, user_id, version): - return self.url + '/v%s/users/%s/labels' % (version, _quote_path(user_id)) - - def _workflow_status_url(self, account_id, run_id): - return (API3_URL + '/v3/accounts/%s/workflows/runs/%s' % - (_quote_path(account_id), _quote_path(run_id))) - - def _get_decisions_url(self, account_id): - return API3_URL + '/v3/accounts/%s/decisions' % (_quote_path(account_id),) - - def _user_decisions_url(self, account_id, user_id): - return (API3_URL + '/v3/accounts/%s/users/%s/decisions' % - (_quote_path(account_id), _quote_path(user_id))) - - def _order_decisions_url(self, account_id, order_id): - return (API3_URL + '/v3/accounts/%s/orders/%s/decisions' % - (_quote_path(account_id), _quote_path(order_id))) - - def _session_decisions_url(self, account_id, user_id, session_id): - return (API3_URL + '/v3/accounts/%s/users/%s/sessions/%s/decisions' % - (_quote_path(account_id), _quote_path(user_id), _quote_path(session_id))) + raise ApiException(str(e), url) - def _content_decisions_url(self, account_id, user_id, content_id): - return (API3_URL + '/v3/accounts/%s/users/%s/content/%s/decisions' % - (_quote_path(account_id), _quote_path(user_id), _quote_path(content_id))) + return Response(response) - def _order_apply_decisions_url(self, account_id, user_id, order_id): - return (API3_URL + '/v3/accounts/%s/users/%s/orders/%s/decisions' % - (_quote_path(account_id), _quote_path(user_id), _quote_path(order_id))) + def verification_check( + self, + properties: Mapping[str, t.Any], + timeout: int | float | tuple[int | float, int | float] | None = None, + version: str | None = None, + ) -> Response: + """ + The verification_check call is used for checking the OTP provided by + the end user to Sift. Sift then compares the OTP, checks rate limits + and responds with a decision whether the user should be able to + proceed or not. - def _session_apply_decisions_url(self, account_id, user_id, session_id): - return (API3_URL + '/v3/accounts/%s/users/%s/sessions/%s/decisions' % - (_quote_path(account_id), _quote_path(user_id), _quote_path(session_id))) + This call is blocking. - def _content_apply_decisions_url(self, account_id, user_id, content_id): - return (API3_URL + '/v3/accounts/%s/users/%s/content/%s/decisions' % - (_quote_path(account_id), _quote_path(user_id), _quote_path(content_id))) + Visit https://developers.sift.com/docs/python/verification-api/check + for more information on our check response structure. - def _psp_merchant_url(self, account_id): - return (self.url + '/v3/accounts/%s/psp_management/merchants' % - (_quote_path(account_id))) + Args: - def _psp_merchant_id_url(self, account_id, merchant_id): - return (self.url + '/v3/accounts/%s/psp_management/merchants/%s' % - (_quote_path(account_id), _quote_path(merchant_id))) + properties: - def _verification_send_url(self): - return (API_URL_VERIFICATION + 'send') - - def _verification_resend_url(self): - return (API_URL_VERIFICATION + 'resend') - - def _verification_check_url(self): - return (API_URL_VERIFICATION + 'check') + $user_id: + User ID of user being verified, e.g. johndoe123. + $code: + The code the user sent to the customer for validation. + $verified_event (optional): + This will be the event type that triggered the verification. + $verified_entity_id (optional): + The ID of the entity impacted by the event being verified. - @staticmethod - def _get_fields_param(include_score_percentiles, include_warnings): - return [ - field for include, field in [ - (include_score_percentiles, 'SCORE_PERCENTILES'), - (include_warnings, 'WARNINGS') - ] if include - ] + timeout (optional): + Use a custom timeout (in seconds) for this call. + version (optional): + Use a different version of the Sift Science API for this call. -class Response(object): - HTTP_CODES_WITHOUT_BODY = [204, 304] + Returns: + A sift.client.Response object if the call succeeded - def __init__(self, http_response): + Raises: + ApiException: + if the call not succeeded """ - Raises ApiException on invalid JSON in Response body or non-2XX HTTP - status code. - """ - # Set defaults. - self.body = None - self.request = None - self.api_status = None - self.api_error_message = None - self.http_status_code = http_response.status_code - self.url = http_response.url - - if (self.http_status_code not in self.HTTP_CODES_WITHOUT_BODY) and http_response.text: - try: - self.body = http_response.json() - if 'status' in self.body: - self.api_status = self.body['status'] - if 'error_message' in self.body: - self.api_error_message = self.body['error_message'] - if 'request' in list(self.body.keys()) and isinstance(self.body['request'], str): - self.request = json.loads(self.body['request']) - except ValueError: - raise ApiException( - 'Failed to parse json response from {0}'.format(self.url), - url=self.url, - http_status_code=self.http_status_code, - body=self.body, - api_status=self.api_status, - api_error_message=self.api_error_message, - request=self.request) - finally: - if int(self.http_status_code) < 200 or int(self.http_status_code) >= 300: - raise ApiException( - '{0} returned non-2XX http status code {1}'.format(self.url, self.http_status_code), - url=self.url, - http_status_code=self.http_status_code, - body=self.body, - api_status=self.api_status, - api_error_message=self.api_error_message, - request=self.request) - - def __str__(self): - return ('{%s "http_status_code": %s}' % - ('' if self.body is None else '"body": ' + - json.dumps(self.body) + ',', str(self.http_status_code))) - - def is_ok(self): - - if self.http_status_code in self.HTTP_CODES_WITHOUT_BODY: - return 204 == self.http_status_code - - # NOTE: Responses from /v3/... endpoints do not contain an API status. - if self.api_status: - return self.api_status == 0 - - return self.http_status_code == 200 - - -class ApiException(Exception): - def __init__(self, message, url, http_status_code=None, body=None, api_status=None, - api_error_message=None, request=None): - Exception.__init__(self, message) - - self.url = url - self.http_status_code = http_status_code - self.body = body - self.api_status = api_status - self.api_error_message = api_error_message - self.request = request + if timeout is None: + timeout = self.timeout + if version is None: + version = self.version -def _assert_non_empty_unicode(val, name, error_cls=None): - error = False - if not isinstance(val, _UNICODE_STRING): - error_cls = error_cls or TypeError - error = True - elif not val: - error_cls = error_cls or ValueError - error = True + self._validate_check_request(properties) - if error: - raise error_cls('{0} must be a non-empty string'.format(name)) + url = self._verification_check_url() + try: + response = self.session.post( + url, + data=json.dumps(properties), + auth=self._auth(), + headers=self._post_headers(version), + timeout=timeout, + ) + except requests.exceptions.RequestException as e: + raise ApiException(str(e), url) -def _assert_non_empty_dict(val, name): - if not isinstance(val, dict): - raise TypeError('{0} must be a non-empty dict'.format(name)) - elif not val: - raise ValueError('{0} must be a non-empty dict'.format(name)) + return Response(response) diff --git a/sift/constants.py b/sift/constants.py new file mode 100644 index 0000000..bceecc8 --- /dev/null +++ b/sift/constants.py @@ -0,0 +1,7 @@ +API_URL = "https://api.sift.com" + +DECISION_SOURCES = ( + "MANUAL_REVIEW", + "AUTOMATED_RULE", + "CHARGEBACK", +) diff --git a/sift/exceptions.py b/sift/exceptions.py new file mode 100644 index 0000000..aad1432 --- /dev/null +++ b/sift/exceptions.py @@ -0,0 +1,24 @@ +from __future__ import annotations + +import typing as t + + +class ApiException(Exception): + def __init__( + self, + message: str, + url: str, + http_status_code: int | None = None, + body: dict[str, t.Any] | None = None, + api_status: int | None = None, + api_error_message: str | None = None, + request: dict[str, t.Any] | None = None, + ) -> None: + Exception.__init__(self, message) + + self.url = url + self.http_status_code = http_status_code + self.body = body + self.api_status = api_status + self.api_error_message = api_error_message + self.request = request diff --git a/sift/utils.py b/sift/utils.py new file mode 100644 index 0000000..40865e3 --- /dev/null +++ b/sift/utils.py @@ -0,0 +1,20 @@ +from __future__ import annotations + +import json +import typing as t +import urllib.parse +from decimal import Decimal + + +def quote_path(s: str) -> str: + # by default, urllib.quote doesn't escape forward slash; pass the + # optional arg to override this + return urllib.parse.quote(s, safe="") + + +class DecimalEncoder(json.JSONEncoder): + def default(self, o: object) -> tuple[str] | t.Any: + if isinstance(o, Decimal): + return (str(o),) + + return super().default(o) diff --git a/sift/version.py b/sift/version.py index d3560c1..e85c97b 100644 --- a/sift/version.py +++ b/sift/version.py @@ -1,2 +1,2 @@ -VERSION = '5.6.1' -API_VERSION = '205' +VERSION = "5.6.1" +API_VERSION = "205" diff --git a/test_integration_app/decisions_api/test_decisions_api.py b/test_integration_app/decisions_api/test_decisions_api.py index 55d2146..0a16364 100644 --- a/test_integration_app/decisions_api/test_decisions_api.py +++ b/test_integration_app/decisions_api/test_decisions_api.py @@ -1,67 +1,80 @@ -import sift +from os import environ as env + import globals -from os import environ as env +import sift + -class DecisionAPI(): +class DecisionAPI: # Get the value of API_KEY from environment variable - api_key = env['API_KEY'] - account_id = env['ACCOUNT_ID'] - client = sift.Client(api_key = api_key, account_id = account_id) + api_key = env["API_KEY"] + account_id = env["ACCOUNT_ID"] + client = sift.Client(api_key=api_key, account_id=account_id) globals.initialize() user_id = globals.user_id session_id = globals.session_id - def apply_user_decision(self): - applyDecisionRequest = { - "decision_id" : "integration_app_watch_account_abuse", - "source" : "MANUAL_REVIEW", - "analyst" : "analyst@example.com", - "description" : "User linked to three other payment abusers and ordering high value items" + def apply_user_decision(self) -> sift.client.Response: + properties = { + "decision_id": "integration_app_watch_account_abuse", + "source": "MANUAL_REVIEW", + "analyst": "analyst@example.com", + "description": "User linked to three other payment abusers and ordering high value items", } - - return self.client.apply_user_decision(self.user_id, applyDecisionRequest) - - def apply_order_decision(self): - applyOrderDecisionRequest = { - "decision_id" : "block_order_payment_abuse", - "source" : "AUTOMATED_RULE", - "description" : "Auto block pending order as score exceeded risk threshold of 90" + + return self.client.apply_user_decision(self.user_id, properties) + + def apply_order_decision(self) -> sift.client.Response: + properties = { + "decision_id": "block_order_payment_abuse", + "source": "AUTOMATED_RULE", + "description": "Auto block pending order as score exceeded risk threshold of 90", } - - return self.client.apply_order_decision(self.user_id, "ORDER-1234567", applyOrderDecisionRequest) - - def apply_session_decision(self): - applySessionDecisionRequest = { - "decision_id" : "integration_app_watch_account_takeover", - "source" : "MANUAL_REVIEW", - "analyst" : "analyst@example.com", - "description" : "compromised account reported to customer service" + + return self.client.apply_order_decision( + self.user_id, "ORDER-1234567", properties + ) + + def apply_session_decision(self) -> sift.client.Response: + properties = { + "decision_id": "integration_app_watch_account_takeover", + "source": "MANUAL_REVIEW", + "analyst": "analyst@example.com", + "description": "compromised account reported to customer service", } - - return self.client.apply_session_decision(self.user_id, self.session_id, applySessionDecisionRequest) - - def apply_content_decision(self): - applyContentDecisionRequest = { - "decision_id" : "integration_app_watch_content_abuse", - "source" : "MANUAL_REVIEW", - "analyst" : "analyst@example.com", - "description" : "fraudulent listing" + + return self.client.apply_session_decision( + self.user_id, self.session_id, properties + ) + + def apply_content_decision(self) -> sift.client.Response: + properties = { + "decision_id": "integration_app_watch_content_abuse", + "source": "MANUAL_REVIEW", + "analyst": "analyst@example.com", + "description": "fraudulent listing", } - - return self.client.apply_content_decision(self.user_id, "content_id", applyContentDecisionRequest) - def get_user_decisions(self): + return self.client.apply_content_decision( + self.user_id, "content_id", properties + ) + + def get_user_decisions(self) -> sift.client.Response: return self.client.get_user_decisions(self.user_id) - def get_order_decisions(self): + def get_order_decisions(self) -> sift.client.Response: return self.client.get_order_decisions("ORDER-1234567") - def get_content_decisions(self): + def get_content_decisions(self) -> sift.client.Response: return self.client.get_content_decisions(self.user_id, "CONTENT_ID") - def get_session_decisions(self): + def get_session_decisions(self) -> sift.client.Response: return self.client.get_session_decisions(self.user_id, "SESSION_ID") - - def get_decisions(self): - return self.client.get_decisions(entity_type='user', limit=10, start_from=5, abuse_types='legacy,payment_abuse') + + def get_decisions(self) -> sift.client.Response: + return self.client.get_decisions( + entity_type="user", + limit=10, + start_from=5, + abuse_types="legacy,payment_abuse", + ) diff --git a/test_integration_app/events_api/test_events_api.py b/test_integration_app/events_api/test_events_api.py index 04607e0..3a065f5 100644 --- a/test_integration_app/events_api/test_events_api.py +++ b/test_integration_app/events_api/test_events_api.py @@ -1,1316 +1,1288 @@ -import sift -import globals - -from os import environ as env +from __future__ import annotations -class EventsAPI(): - # Get the value of API_KEY from environment variable - api_key = env['API_KEY'] - client = sift.Client(api_key = api_key) - globals.initialize() - user_id = globals.user_id - user_email = globals.user_email - - def add_item_to_cart(self): - add_item_to_cart_properties = { - # Required Fields - "$user_id" : self.user_id, - # Supported Fields - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$item" : { - "$item_id" : "B004834GQO", - "$product_title" : "The Slanket Blanket-Texas Tea", - "$price" : 39990000, # $39.99 - "$currency_code" : "USD", - "$upc" : "6786211451001", - "$sku" : "004834GQ", - "$brand" : "Slanket", - "$manufacturer" : "Slanket", - "$category" : "Blankets & Throws", - "$tags" : ["Awesome", "Wintertime specials"], - "$color" : "Texas Tea", - "$quantity" : 16 - }, - "$brand_name" : "sift", - "$site_domain" : "sift.com", - "$site_country" : "US", - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$add_item_to_cart", add_item_to_cart_properties) - +import os +import typing as t - def add_promotion(self): - add_promotion_properties = { - # Required fields. - "$user_id" : self.user_id, - # Supported fields. - "$promotions" : [ - # Example of a promotion for monetary discounts off good or services - { - "$promotion_id" : "NewRideDiscountMay2016", - "$status" : "$success", - "$description" : "$5 off your first 5 rides", - "$referrer_user_id" : "elon-m93903", - "$discount" : { - "$amount" : 5000000, # $5 - "$currency_code" : "USD" - } - } - ], - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$add_promotion", add_promotion_properties) - - def chargeback(self): - # Sample $chargeback event - chargeback_properties = { - # Required Fields - "$order_id" : "ORDER-123124124", - "$transaction_id" : "719637215", - # Recommended Fields - "$user_id" : self.user_id, - "$chargeback_state" : "$lost", - "$chargeback_reason" : "$duplicate" - } - return self.client.track("$chargeback", chargeback_properties) +import globals - def content_status(self): - # Sample $content_status event - content_status_properties = { - # Required Fields - "$user_id" : self.user_id, - "$content_id" : "9671500641", - "$status" : "$paused", +import sift - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$content_status", content_status_properties) - def create_account(self): - # Sample $create_account event - create_account_properties = { - # Required Fields - "$user_id" : self.user_id, - # Supported Fields - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$user_email" : self.user_email, - "$verification_phone_number" : "+123456789012", - "$name" : "Bill Jones", - "$phone" : "1-415-555-6040", - "$referrer_user_id" : "janejane101", - "$payment_methods" : [ - { - "$payment_type" : "$credit_card", - "$card_bin" : "542486", - "$card_last4" : "4444" +class EventsAPI: + # Get the value of API_KEY from environment variable + api_key = os.environ["API_KEY"] + client = sift.Client(api_key=api_key) + globals.initialize() + user_id = globals.user_id + user_email = globals.user_email + + def add_item_to_cart(self) -> sift.client.Response: + add_item_to_cart_properties = { + # Required Fields + "$user_id": self.user_id, + # Supported Fields + "$session_id": "gigtleqddo84l8cm15qe4il", + "$item": { + "$item_id": "B004834GQO", + "$product_title": "The Slanket Blanket-Texas Tea", + "$price": 39990000, # $39.99 + "$currency_code": "USD", + "$upc": "6786211451001", + "$sku": "004834GQ", + "$brand": "Slanket", + "$manufacturer": "Slanket", + "$category": "Blankets & Throws", + "$tags": ["Awesome", "Wintertime specials"], + "$color": "Texas Tea", + "$quantity": 16, + }, + "$brand_name": "sift", + "$site_domain": "sift.com", + "$site_country": "US", + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - "$billing_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6040", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$shipping_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$promotions" : [ - { - "$promotion_id" : "FriendReferral", - "$status" : "$success", - "$referrer_user_id" : "janejane102", - "$credit_point" : { - "$amount" : 100, - "$credit_point_type" : "account karma" - } + return self.client.track( + "$add_item_to_cart", add_item_to_cart_properties + ) + + def add_promotion(self) -> sift.client.Response: + add_promotion_properties = { + # Required fields. + "$user_id": self.user_id, + # Supported fields. + "$promotions": [ + # Example of a promotion for monetary discounts off good or services + { + "$promotion_id": "NewRideDiscountMay2016", + "$status": "$success", + "$description": "$5 off your first 5 rides", + "$referrer_user_id": "elon-m93903", + "$discount": { + "$amount": 5000000, + "$currency_code": "USD", + }, # $5 + } + ], + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - "$social_sign_on_type" : "$twitter", - "$account_types" : ["merchant", "premium"], - - # Suggested Custom Fields - "twitter_handle" : "billyjones", - "work_phone" : "1-347-555-5921", - "location" : "New London, NH", - "referral_code" : "MIKEFRIENDS", - "email_confirmed_status" : "$pending", - "phone_confirmed_status" : "$pending", - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$create_account", create_account_properties) - - def create_content_comment(self): - # Sample $create_content event for comments - comment_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "comment-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $comment object - "$comment" : { - "$body" : "Congrats on the new role!", - "$contact_email" : "alex_301@domain.com", - "$parent_comment_id" : "comment-23407", - "$root_content_id" : "listing-12923213", - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "An old picture" - } - ] - }, - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$create_content", comment_properties) - - def create_content_listing(self): - # Sample $create_content event for listings - listing_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "listing-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $listing object - "$listing" : { - "$subject" : "2 Bedroom Apartment for Rent", - "$body" : "Capitol Hill Seattle brand new condo. 2 bedrooms and 1 full bath.", - "$contact_email" : "alex_301@domain.com", - "$contact_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$locations" : [ - { - "$city" : "Seattle", - "$region" : "Washington", - "$country" : "US", - "$zipcode" : "98112" - } - ], - "$listed_items" : [ - { - "$price" : 2950000000, # $2950.00 - "$currency_code" : "USD", - "$tags" : ["heat", "washer/dryer"] - } - ], - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "Billy's picture" - } - ], - "$expiration_time" : 1549063157000 # UNIX timestamp in milliseconds - }, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$create_content", listing_properties) - - def create_content_message(self): - # Sample $create_content event for messages - message_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "message-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $message object - "$message" : { - "$body" : "Let’s meet at 5pm", - "$contact_email" : "alex_301@domain.com", - "$recipient_user_ids" : ["fy9h989sjphh71"], - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "My hike today!" - } - ] - }, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" + return self.client.track("$add_promotion", add_promotion_properties) + + def chargeback(self) -> sift.client.Response: + # Sample $chargeback event + chargeback_properties = { + # Required Fields + "$order_id": "ORDER-123124124", + "$transaction_id": "719637215", + # Recommended Fields + "$user_id": self.user_id, + "$chargeback_state": "$lost", + "$chargeback_reason": "$duplicate", } - } - return self.client.track("$create_content", message_properties) - - def create_content_post(self): - # Sample $create_content event for posts - post_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "post-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $post object - "$post" : { - "$subject" : "My new apartment!", - "$body" : "Moved into my new apartment yesterday.", - "$contact_email" : "alex_301@domain.com", - "$contact_address" : { - "$name" : "Bill Jones", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$locations" : [ - { - "$city" : "Seattle", - "$region" : "Washington", - "$country" : "US", - "$zipcode" : "98112" - } - ], - "$categories" : ["Personal"], - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "View from the window!" - } - ], - "$expiration_time" : 1549063157000 # UNIX timestamp in milliseconds - }, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$create_content", post_properties) - - def create_content_profile(self): - # Sample $create_content event for reviews - profile_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "profile-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $profile object - "$profile" : { - "$body" : "Hi! My name is Alex and I just moved to New London!", - "$contact_email" : "alex_301@domain.com", - "$contact_address" : { - "$name" : "Alex Smith", - "$phone" : "1-415-555-6041", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "Alex's picture" - } - ], - "$categories" : [ - "Friends", - "Long-term dating" - ] - }, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$create_content", profile_properties) - - def create_content_review(self): - # Sample $create_content event for reviews - review_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "review-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $review object - "$review" : { - "$subject" : "Amazing Tacos!", - "$body" : "I ate the tacos.", - "$contact_email" : "alex_301@domain.com", - "$locations" : [ - { - "$city" : "Seattle", - "$region" : "Washington", - "$country" : "US", - "$zipcode" : "98112" - } - ], - "$reviewed_content_id" : "listing-234234", - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "Calamari tacos." - } - ], - "$rating" : 4.5 - }, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$create_content", review_properties) - - def create_order(self): - # Sample $create_order event - order_properties = self.build_create_order_event() - return self.client.track("$create_order", order_properties) - - def create_order_with_warnings(self): - # Sample $create_order event - order_properties = self.build_create_order_event() - return self.client.track("$create_order", order_properties, include_warnings=True) - - def build_create_order_event(self): - order_properties = { - # Required Fields - "$user_id": self.user_id, - # Supported Fields - "$session_id": "gigtleqddo84l8cm15qe4il", - "$order_id": "ORDER-28168441", - "$user_email": self.user_email, - "$verification_phone_number": "+123456789012", - "$amount": 115940000, # $115.94 - "$currency_code": "USD", - "$billing_address": { - "$name": "Bill Jones", - "$phone": "1-415-555-6041", - "$address_1": "2100 Main Street", - "$address_2": "Apt 3B", - "$city": "New London", - "$region": "New Hampshire", - "$country": "US", - "$zipcode": "03257" - }, - "$payment_methods": [ - { - "$payment_type": "$credit_card", - "$payment_gateway": "$braintree", - "$card_bin": "542486", - "$card_last4": "4444" + return self.client.track("$chargeback", chargeback_properties) + + def content_status(self) -> sift.client.Response: + # Sample $content_status event + content_status_properties = { + # Required Fields + "$user_id": self.user_id, + "$content_id": "9671500641", + "$status": "$paused", + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - "$ordered_from": { - "$store_id": "123", - "$store_address": { - "$name": "Bill Jones", - "$phone": "1-415-555-6040", - "$address_1": "2100 Main Street", - "$address_2": "Apt 3B", - "$city": "New London", - "$region": "New Hampshire", - "$country": "US", - "$zipcode": "03257" + return self.client.track("$content_status", content_status_properties) + + def create_account(self) -> sift.client.Response: + # Sample $create_account event + create_account_properties = { + # Required Fields + "$user_id": self.user_id, + # Supported Fields + "$session_id": "gigtleqddo84l8cm15qe4il", + "$user_email": self.user_email, + "$verification_phone_number": "+123456789012", + "$name": "Bill Jones", + "$phone": "1-415-555-6040", + "$referrer_user_id": "janejane101", + "$payment_methods": [ + { + "$payment_type": "$credit_card", + "$card_bin": "542486", + "$card_last4": "4444", + } + ], + "$billing_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6040", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$shipping_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$promotions": [ + { + "$promotion_id": "FriendReferral", + "$status": "$success", + "$referrer_user_id": "janejane102", + "$credit_point": { + "$amount": 100, + "$credit_point_type": "account karma", + }, + } + ], + "$social_sign_on_type": "$twitter", + "$account_types": ["merchant", "premium"], + # Suggested Custom Fields + "twitter_handle": "billyjones", + "work_phone": "1-347-555-5921", + "location": "New London, NH", + "referral_code": "MIKEFRIENDS", + "email_confirmed_status": "$pending", + "phone_confirmed_status": "$pending", + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - }, - "$brand_name": "sift", - "$site_domain": "sift.com", - "$site_country": "US", - "$shipping_address": { - "$name": "Bill Jones", - "$phone": "1-415-555-6041", - "$address_1": "2100 Main Street", - "$address_2": "Apt 3B", - "$city": "New London", - "$region": "New Hampshire", - "$country": "US", - "$zipcode": "03257" - }, - "$expedited_shipping": True, - "$shipping_method": "$physical", - "$shipping_carrier": "UPS", - "$shipping_tracking_numbers": ["1Z204E380338943508", "1Z204E380338943509"], - "$items": [ - { - "$item_id": "12344321", - "$product_title": "Microwavable Kettle Corn: Original Flavor", - "$price": 4990000, # $4.99 - "$upc": "097564307560", - "$sku": "03586005", - "$brand": "Peters Kettle Corn", - "$manufacturer": "Peters Kettle Corn", - "$category": "Food and Grocery", - "$tags": ["Popcorn", "Snacks", "On Sale"], - "$quantity": 4 - }, - { - "$item_id": "B004834GQO", - "$product_title": "The Slanket Blanket-Texas Tea", - "$price": 39990000, # $39.99 - "$upc": "6786211451001", - "$sku": "004834GQ", - "$brand": "Slanket", - "$manufacturer": "Slanket", - "$category": "Blankets & Throws", - "$tags": ["Awesome", "Wintertime specials"], - "$color": "Texas Tea", - "$quantity": 2 + return self.client.track("$create_account", create_account_properties) + + def create_content_comment(self) -> sift.client.Response: + # Sample $create_content event for comments + comment_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "comment-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $comment object + "$comment": { + "$body": "Congrats on the new role!", + "$contact_email": "alex_301@domain.com", + "$parent_comment_id": "comment-23407", + "$root_content_id": "listing-12923213", + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "An old picture", + } + ], + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - # For marketplaces, use $seller_user_id to identify the seller - "$seller_user_id": "slinkys_emporium", - - "$promotions": [ - { - "$promotion_id": "FirstTimeBuyer", - "$status": "$success", - "$description": "$5 off", - "$discount": { - "$amount": 5000000, # $5.00 + return self.client.track("$create_content", comment_properties) + + def create_content_listing(self) -> sift.client.Response: + # Sample $create_content event for listings + listing_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "listing-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $listing object + "$listing": { + "$subject": "2 Bedroom Apartment for Rent", + "$body": "Capitol Hill Seattle brand new condo. 2 bedrooms and 1 full bath.", + "$contact_email": "alex_301@domain.com", + "$contact_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$locations": [ + { + "$city": "Seattle", + "$region": "Washington", + "$country": "US", + "$zipcode": "98112", + } + ], + "$listed_items": [ + { + "$price": 2950000000, # $2950.00 + "$currency_code": "USD", + "$tags": ["heat", "washer/dryer"], + } + ], + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "Billy's picture", + } + ], + "$expiration_time": 1549063157000, # UNIX timestamp in milliseconds + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } + return self.client.track("$create_content", listing_properties) + + def create_content_message(self) -> sift.client.Response: + # Sample $create_content event for messages + message_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "message-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $message object + "$message": { + "$body": "Let’s meet at 5pm", + "$contact_email": "alex_301@domain.com", + "$recipient_user_ids": ["fy9h989sjphh71"], + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "My hike today!", + } + ], + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } + return self.client.track("$create_content", message_properties) + + def create_content_post(self) -> sift.client.Response: + # Sample $create_content event for posts + post_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "post-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $post object + "$post": { + "$subject": "My new apartment!", + "$body": "Moved into my new apartment yesterday.", + "$contact_email": "alex_301@domain.com", + "$contact_address": { + "$name": "Bill Jones", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$locations": [ + { + "$city": "Seattle", + "$region": "Washington", + "$country": "US", + "$zipcode": "98112", + } + ], + "$categories": ["Personal"], + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "View from the window!", + } + ], + "$expiration_time": 1549063157000, # UNIX timestamp in milliseconds + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } + return self.client.track("$create_content", post_properties) + + def create_content_profile(self) -> sift.client.Response: + # Sample $create_content event for reviews + profile_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "profile-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $profile object + "$profile": { + "$body": "Hi! My name is Alex and I just moved to New London!", + "$contact_email": "alex_301@domain.com", + "$contact_address": { + "$name": "Alex Smith", + "$phone": "1-415-555-6041", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "Alex's picture", + } + ], + "$categories": ["Friends", "Long-term dating"], + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } + return self.client.track("$create_content", profile_properties) + + def create_content_review(self) -> sift.client.Response: + # Sample $create_content event for reviews + review_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "review-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $review object + "$review": { + "$subject": "Amazing Tacos!", + "$body": "I ate the tacos.", + "$contact_email": "alex_301@domain.com", + "$locations": [ + { + "$city": "Seattle", + "$region": "Washington", + "$country": "US", + "$zipcode": "98112", + } + ], + "$reviewed_content_id": "listing-234234", + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "Calamari tacos.", + } + ], + "$rating": 4.5, + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } + return self.client.track("$create_content", review_properties) + + def create_order(self) -> sift.client.Response: + # Sample $create_order event + order_properties = self.build_create_order_event() + return self.client.track("$create_order", order_properties) + + def create_order_with_warnings(self) -> sift.client.Response: + # Sample $create_order event + order_properties = self.build_create_order_event() + return self.client.track( + "$create_order", order_properties, include_warnings=True + ) + + def build_create_order_event(self) -> dict[str, t.Any]: + order_properties = { + # Required Fields + "$user_id": self.user_id, + # Supported Fields + "$session_id": "gigtleqddo84l8cm15qe4il", + "$order_id": "ORDER-28168441", + "$user_email": self.user_email, + "$verification_phone_number": "+123456789012", + "$amount": 115940000, # $115.94 "$currency_code": "USD", - "$minimum_purchase_amount": 25000000 # $25.00 - } + "$billing_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$payment_methods": [ + { + "$payment_type": "$credit_card", + "$payment_gateway": "$braintree", + "$card_bin": "542486", + "$card_last4": "4444", + } + ], + "$ordered_from": { + "$store_id": "123", + "$store_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6040", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + }, + "$brand_name": "sift", + "$site_domain": "sift.com", + "$site_country": "US", + "$shipping_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$expedited_shipping": True, + "$shipping_method": "$physical", + "$shipping_carrier": "UPS", + "$shipping_tracking_numbers": [ + "1Z204E380338943508", + "1Z204E380338943509", + ], + "$items": [ + { + "$item_id": "12344321", + "$product_title": "Microwavable Kettle Corn: Original Flavor", + "$price": 4990000, # $4.99 + "$upc": "097564307560", + "$sku": "03586005", + "$brand": "Peters Kettle Corn", + "$manufacturer": "Peters Kettle Corn", + "$category": "Food and Grocery", + "$tags": ["Popcorn", "Snacks", "On Sale"], + "$quantity": 4, + }, + { + "$item_id": "B004834GQO", + "$product_title": "The Slanket Blanket-Texas Tea", + "$price": 39990000, # $39.99 + "$upc": "6786211451001", + "$sku": "004834GQ", + "$brand": "Slanket", + "$manufacturer": "Slanket", + "$category": "Blankets & Throws", + "$tags": ["Awesome", "Wintertime specials"], + "$color": "Texas Tea", + "$quantity": 2, + }, + ], + # For marketplaces, use $seller_user_id to identify the seller + "$seller_user_id": "slinkys_emporium", + "$promotions": [ + { + "$promotion_id": "FirstTimeBuyer", + "$status": "$success", + "$description": "$5 off", + "$discount": { + "$amount": 5000000, # $5.00 + "$currency_code": "USD", + "$minimum_purchase_amount": 25000000, # $25.00 + }, + } + ], + # Sample Custom Fields + "digital_wallet": "apple_pay", # "google_wallet", etc. + "coupon_code": "dollarMadness", + "shipping_choice": "FedEx Ground Courier", + "is_first_time_buyer": False, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - - # Sample Custom Fields - "digital_wallet": "apple_pay", # "google_wallet", etc. - "coupon_code": "dollarMadness", - "shipping_choice": "FedEx Ground Courier", - "is_first_time_buyer": False, - - # Send this information from a BROWSER client. - "$browser": { - "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language": "en-US", - "$content_language": "en-GB" - } - } - return order_properties - - def flag_content(self): - # Sample $flag_content event - flag_content_properties = { - # Required Fields - "$user_id" : self.user_id, # content creator - "$content_id" : "9671500641", - - # Supported Fields - "$flagged_by" : "jamieli89" - } - return self.client.track("$flag_content", flag_content_properties) - - def link_session_to_user(self): - # Sample $link_session_to_user event - link_session_to_user_properties = { - # Required Fields - "$user_id" : self.user_id, - "$session_id" : "gigtleqddo84l8cm15qe4il" - } - return self.client.track("$link_session_to_user", link_session_to_user_properties) - - def login(self): - # Sample $login event - login_properties = { - # Required Fields - "$user_id" : self.user_id, - "$login_status" : "$failure", - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$ip" : "128.148.1.135", - - # Optional Fields - "$user_email" : self.user_email, - "$verification_phone_number" : "+123456789012", - "$failure_reason" : "$wrong_password", - "$username" : "billjones1@example.com", - "$account_types" : ["merchant", "premium"], - "$social_sign_on_type" : "$linkedin", - "$brand_name" : "sift", - "$site_domain" : "sift.com", - "$site_country" : "US", - - # Send this information with a login from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$login", login_properties) - - def logout(self): - # Sample $logout event - logout_properties = { - # Required Fields - "$user_id" : self.user_id, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$logout", logout_properties) - - def order_status(self): - # Sample $order_status event - order_properties = { - # Required Fields - "$user_id" : self.user_id, - "$order_id" : "ORDER-28168441", - "$order_status" : "$canceled", - - # Optional Fields - "$reason" : "$payment_risk", - "$source" : "$manual_review", - "$analyst" : "someone@your-site.com", - "$webhook_id" : "3ff1082a4aea8d0c58e3643ddb7a5bb87ffffeb2492dca33", - "$description" : "Canceling because multiple fraudulent users on device", - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$order_status", order_properties) - - def remove_item_from_cart(self): - # Sample $remove_item_from_cart event - remove_item_from_cart_properties = { - # Required Fields - "$user_id" : self.user_id, - - # Supported Fields - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$item" : { - "$item_id" : "B004834GQO", - "$product_title" : "The Slanket Blanket-Texas Tea", - "$price" : 39990000, # $39.99 - "$currency_code" : "USD", - "$quantity" : 2, - "$upc" : "6786211451001", - "$sku" : "004834GQ", - "$brand" : "Slanket", - "$manufacturer" : "Slanket", - "$category" : "Blankets & Throws", - "$tags" : ["Awesome", "Wintertime specials"], - "$color" : "Texas Tea" - }, - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - - return self.client.track("$remove_item_from_cart", remove_item_from_cart_properties) - - def security_notification(self): - # Sample $security_notification event - security_notification_properties = { - # Required Fields - "$user_id" : self.user_id, - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$notification_status" : "$sent", - # Optional fields if applicable - "$notification_type" : "$email", - "$notified_value" : "billy123@domain.com", - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - - return self.client.track("$security_notification", security_notification_properties) - - def transaction(self): - # Sample $transaction event - transaction_properties = { - # Required Fields - "$user_id" : self.user_id, - "$amount" : 506790000, # $506.79 - "$currency_code" : "USD", - # Supported Fields - "$user_email" : self.user_email, - "$verification_phone_number" : "+123456789012", - "$transaction_type" : "$sale", - "$transaction_status" : "$failure", - "$decline_category" : "$bank_decline", - "$order_id" : "ORDER-123124124", - "$transaction_id" : "719637215", - "$billing_address" : { # or "$sent_address" # or "$received_address" - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$brand_name" : "sift", - "$site_domain" : "sift.com", - "$site_country" : "US", - "$ordered_from" : { - "$store_id" : "123", - "$store_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6040", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" + return order_properties + + def flag_content(self) -> sift.client.Response: + # Sample $flag_content event + flag_content_properties = { + # Required Fields + "$user_id": self.user_id, # content creator + "$content_id": "9671500641", + # Supported Fields + "$flagged_by": "jamieli89", } - }, - # Credit card example - "$payment_method" : { - "$payment_type" : "$credit_card", - "$payment_gateway" : "$braintree", - "$card_bin" : "542486", - "$card_last4" : "4444" - }, - - # Supported fields for 3DS - "$status_3ds" : "$attempted", - "$triggered_3ds" : "$processor", - "$merchant_initiated_transaction" : False, - - # Supported Fields - "$shipping_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$session_id" : "gigtleqddo84l8cm15qe4il", - - # For marketplaces, use $seller_user_id to identify the seller - "$seller_user_id" : "slinkys_emporium", - - # Sample Custom Fields - "digital_wallet" : "apple_pay", # "google_wallet", etc. - "coupon_code" : "dollarMadness", - "shipping_choice" : "FedEx Ground Courier", - "is_first_time_buyer" : False, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - return self.client.track("$transaction", transaction_properties) - - def update_account(self): - # Sample $update_account event - update_account_properties = { - # Required Fields - "$user_id" : self.user_id, - - # Supported Fields - "$changed_password" : True, - "$user_email" : self.user_email, - "$verification_phone_number" : "+123456789012", - "$name" : "Bill Jones", - "$phone" : "1-415-555-6040", - "$referrer_user_id" : "janejane102", - "$payment_methods" : [ - { - "$payment_type" : "$credit_card", - "$card_bin" : "542486", - "$card_last4" : "4444" + return self.client.track("$flag_content", flag_content_properties) + + def link_session_to_user(self) -> sift.client.Response: + # Sample $link_session_to_user event + link_session_to_user_properties = { + # Required Fields + "$user_id": self.user_id, + "$session_id": "gigtleqddo84l8cm15qe4il", } - ], - "$billing_address" : - { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$shipping_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - - "$social_sign_on_type" : "$twitter", - "$account_types" : ["merchant", "premium"], - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - - return self.client.track("$update_account", update_account_properties) - - def update_content_comment(self): - # Sample $update_content event for comments - update_content_comment_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "comment-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $comment object - "$comment" : { - "$body" : "Congrats on the new role!", - "$contact_email" : "alex_301@domain.com", - "$parent_comment_id" : "comment-23407", - "$root_content_id" : "listing-12923213", - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "An old picture" - } - ] - }, - # Send this information from an APP client. - "$app" : { - # Example for the iOS Calculator app. - "$os" : "iOS", - "$os_version" : "10.1.3", - "$device_manufacturer" : "Apple", - "$device_model" : "iPhone 4,2", - "$device_unique_id" : "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", - "$app_name" : "Calculator", - "$app_version" : "3.2.7", - "$client_language" : "en-US" - } - } - return self.client.track("$update_content", update_content_comment_properties) - - def update_content_listing(self): - # Sample $update_content event for listings - update_content_listing_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "listing-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $listing object - "$listing" : { - "$subject" : "2 Bedroom Apartment for Rent", - "$body" : "Capitol Hill Seattle brand new condo. 2 bedrooms and 1 full bath.", - "$contact_email" : "alex_301@domain.com", - "$contact_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$locations" : [ - { - "$city" : "Seattle", - "$region" : "Washington", - "$country" : "US", - "$zipcode" : "98112" - } - ], - "$listed_items" : [ - { - "$price" : 2950000000, # $2950.00 - "$currency_code" : "USD", - "$tags" : ["heat", "washer/dryer"] - } - ], - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "Billy's picture" - } - ], - "$expiration_time" : 1549063157000 # UNIX timestamp in milliseconds - }, - # Send this information from an APP client. - "$app" : { - # Example for the iOS Calculator app. - "$os" : "iOS", - "$os_version" : "10.1.3", - "$device_manufacturer" : "Apple", - "$device_model" : "iPhone 4,2", - "$device_unique_id" : "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", - "$app_name" : "Calculator", - "$app_version" : "3.2.7", - "$client_language" : "en-US" - } - } - return self.client.track("$update_content", update_content_listing_properties) - - def update_content_message(self): - # Sample $update_content event for messages - update_content_message_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "message-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $message object - "$message" : { - "$body" : "Lets meet at 5pm", - "$contact_email" : "alex_301@domain.com", - "$recipient_user_ids" : ["fy9h989sjphh71"], - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "My hike today!" - } - ] - }, - - # Send this information from a BROWSER client. - "$browser" : { - "$user_agent" : "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", - "$accept_language" : "en-US", - "$content_language" : "en-GB" - } - } - - return self.client.track("$update_content", update_content_message_properties) - - def update_content_post(self): - # Sample $update_content event for posts - update_content_post_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "post-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $post object - "$post" : { - "$subject" : "My new apartment!", - "$body" : "Moved into my new apartment yesterday.", - "$contact_email" : "alex_301@domain.com", - "$contact_address" : { - "$name" : "Bill Jones", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$locations" : [ - { - "$city" : "Seattle", - "$region" : "Washington", - "$country" : "US", - "$zipcode" : "98112" - } - ], - "$categories" : ["Personal"], - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "View from the window!" - } - ], - "$expiration_time" : 1549063157000 # UNIX timestamp in milliseconds - }, - # Send this information from an APP client. - "$app" : { - # Example for the iOS Calculator app. - "$os" : "iOS", - "$os_version" : "10.1.3", - "$device_manufacturer" : "Apple", - "$device_model" : "iPhone 4,2", - "$device_unique_id" : "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", - "$app_name" : "Calculator", - "$app_version" : "3.2.7", - "$client_language" : "en-US" - } - } - return self.client.track("$update_content", update_content_post_properties) - - def update_content_profile(self): - # Sample $update_content event for reviews - update_content_profile_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "profile-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $profile object - "$profile" : { - "$body" : "Hi! My name is Alex and I just moved to New London!", - "$contact_email" : "alex_301@domain.com", - "$contact_address" : { - "$name" : "Alex Smith", - "$phone" : "1-415-555-6041", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "Alex's picture" - } - ], - "$categories" : [ - "Friends", - "Long-term dating" - ] - }, - # ========================================= - # Send this information from an APP client. - "$app" : { - # Example for the iOS Calculator app. - "$os" : "iOS", - "$os_version" : "10.1.3", - "$device_manufacturer" : "Apple", - "$device_model" : "iPhone 4,2", - "$device_unique_id" : "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", - "$app_name" : "Calculator", - "$app_version" : "3.2.7", - "$client_language" : "en-US" - } - } - return self.client.track("$update_content", update_content_profile_properties) - - def update_content_review(self): - # Sample $update_content event for reviews - update_content_review_properties = { - # Required fields - "$user_id" : self.user_id, - "$content_id" : "review-23412", - - # Recommended fields - "$session_id" : "a234ksjfgn435sfg", - "$status" : "$active", - "$ip" : "255.255.255.0", - - # Required $review object - "$review" : { - "$subject" : "Amazing Tacos!", - "$body" : "I ate the tacos.", - "$contact_email" : "alex_301@domain.com", - "$locations" : [ - { - "$city" : "Seattle", - "$region" : "Washington", - "$country" : "US", - "$zipcode" : "98112" - } - ], - "$reviewed_content_id" : "listing-234234", - "$images" : [ - { - "$md5_hash" : "0cc175b9c0f1b6a831c399e269772661", - "$link" : "https://www.domain.com/file.png", - "$description" : "Calamari tacos." - } - ], - "$rating" : 4.5 - }, - # Send this information from an APP client. - "$app" : { - # Example for the iOS Calculator app. - "$os" : "iOS", - "$os_version" : "10.1.3", - "$device_manufacturer" : "Apple", - "$device_model" : "iPhone 4,2", - "$device_unique_id" : "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", - "$app_name" : "Calculator", - "$app_version" : "3.2.7", - "$client_language" : "en-US" - } - } - return self.client.track("$update_content", update_content_review_properties) - - def update_order(self): - # Sample $update_order event - update_order_properties = { - # Required Fields - "$user_id" : self.user_id, - # Supported Fields - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$order_id" : "ORDER-28168441", - "$user_email" : self.user_email, - "$verification_phone_number" : "+123456789012", - "$amount" : 115940000, # $115.94 - "$currency_code" : "USD", - "$billing_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$payment_methods" : [ - { - "$payment_type" : "$credit_card", - "$payment_gateway" : "$braintree", - "$card_bin" : "542486", - "$card_last4" : "4444" + return self.client.track( + "$link_session_to_user", link_session_to_user_properties + ) + + def login(self) -> sift.client.Response: + # Sample $login event + login_properties = { + # Required Fields + "$user_id": self.user_id, + "$login_status": "$failure", + "$session_id": "gigtleqddo84l8cm15qe4il", + "$ip": "128.148.1.135", + # Optional Fields + "$user_email": self.user_email, + "$verification_phone_number": "+123456789012", + "$failure_reason": "$wrong_password", + "$username": "billjones1@example.com", + "$account_types": ["merchant", "premium"], + "$social_sign_on_type": "$linkedin", + "$brand_name": "sift", + "$site_domain": "sift.com", + "$site_country": "US", + # Send this information with a login from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - "$brand_name" : "sift", - "$site_domain" : "sift.com", - "$site_country" : "US", - "$ordered_from" : { - "$store_id" : "123", - "$store_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6040", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" + return self.client.track("$login", login_properties) + + def logout(self) -> sift.client.Response: + # Sample $logout event + logout_properties = { + # Required Fields + "$user_id": self.user_id, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - }, - "$shipping_address" : { - "$name" : "Bill Jones", - "$phone" : "1-415-555-6041", - "$address_1" : "2100 Main Street", - "$address_2" : "Apt 3B", - "$city" : "New London", - "$region" : "New Hampshire", - "$country" : "US", - "$zipcode" : "03257" - }, - "$expedited_shipping" : True, - "$shipping_method" : "$physical", - "$shipping_carrier" : "UPS", - "$shipping_tracking_numbers": ["1Z204E380338943508", "1Z204E380338943509"], - "$items" : [ - { - "$item_id" : "12344321", - "$product_title" : "Microwavable Kettle Corn: Original Flavor", - "$price" : 4990000, # $4.99 - "$upc" : "097564307560", - "$sku" : "03586005", - "$brand" : "Peters Kettle Corn", - "$manufacturer" : "Peters Kettle Corn", - "$category" : "Food and Grocery", - "$tags" : ["Popcorn", "Snacks", "On Sale"], - "$quantity" : 4 - }, - { - "$item_id" : "B004834GQO", - "$product_title" : "The Slanket Blanket-Texas Tea", - "$price" : 39990000, # $39.99 - "$upc" : "6786211451001", - "$sku" : "004834GQ", - "$brand" : "Slanket", - "$manufacturer" : "Slanket", - "$category" : "Blankets & Throws", - "$tags" : ["Awesome", "Wintertime specials"], - "$color" : "Texas Tea", - "$quantity" : 2 + return self.client.track("$logout", logout_properties) + + def order_status(self) -> sift.client.Response: + # Sample $order_status event + order_properties = { + # Required Fields + "$user_id": self.user_id, + "$order_id": "ORDER-28168441", + "$order_status": "$canceled", + # Optional Fields + "$reason": "$payment_risk", + "$source": "$manual_review", + "$analyst": "someone@your-site.com", + "$webhook_id": "3ff1082a4aea8d0c58e3643ddb7a5bb87ffffeb2492dca33", + "$description": "Canceling because multiple fraudulent users on device", + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } + return self.client.track("$order_status", order_properties) + + def remove_item_from_cart(self) -> sift.client.Response: + # Sample $remove_item_from_cart event + remove_item_from_cart_properties = { + # Required Fields + "$user_id": self.user_id, + # Supported Fields + "$session_id": "gigtleqddo84l8cm15qe4il", + "$item": { + "$item_id": "B004834GQO", + "$product_title": "The Slanket Blanket-Texas Tea", + "$price": 39990000, # $39.99 + "$currency_code": "USD", + "$quantity": 2, + "$upc": "6786211451001", + "$sku": "004834GQ", + "$brand": "Slanket", + "$manufacturer": "Slanket", + "$category": "Blankets & Throws", + "$tags": ["Awesome", "Wintertime specials"], + "$color": "Texas Tea", + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - # For marketplaces, use $seller_user_id to identify the seller - "$seller_user_id" : "slinkys_emporium", - "$promotions" : [ - { - "$promotion_id" : "FirstTimeBuyer", - "$status" : "$success", - "$description" : "$5 off", - "$discount" : { - "$amount" : 5000000, # $5.00 - "$currency_code" : "USD", - "$minimum_purchase_amount" : 25000000 # $25.00 - } + return self.client.track( + "$remove_item_from_cart", remove_item_from_cart_properties + ) + + def security_notification(self) -> sift.client.Response: + # Sample $security_notification event + security_notification_properties = { + # Required Fields + "$user_id": self.user_id, + "$session_id": "gigtleqddo84l8cm15qe4il", + "$notification_status": "$sent", + # Optional fields if applicable + "$notification_type": "$email", + "$notified_value": "billy123@domain.com", + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, } - ], - # Sample Custom Fields - "digital_wallet" : "apple_pay", # "google_wallet", etc. - "coupon_code" : "dollarMadness", - "shipping_choice" : "FedEx Ground Courier", - "is_first_time_buyer" : False, - # Send this information from an APP client. - "$app" : { - # Example for the iOS Calculator app. - "$os" : "iOS", - "$os_version" : "10.1.3", - "$device_manufacturer" : "Apple", - "$device_model" : "iPhone 4,2", - "$device_unique_id" : "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", - "$app_name" : "Calculator", - "$app_version" : "3.2.7", - "$client_language" : "en-US" - } - } - return self.client.track("$update_order", update_order_properties) + return self.client.track( + "$security_notification", security_notification_properties + ) - def update_password(self): - # Sample $update_password event - update_password_properties = { - # Required Fields - "$user_id" : self.user_id, - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$status" : "$success", - "$reason" : "$forced_reset", - "$ip" : "128.148.1.135", # IP of the user that entered the new password after the old password was reset - # Send this information from an APP client. - "$app" : { - # Example for the iOS Calculator app. - "$os" : "iOS", - "$os_version" : "10.1.3", - "$device_manufacturer" : "Apple", - "$device_model" : "iPhone 4,2", - "$device_unique_id" : "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", - "$app_name" : "Calculator", - "$app_version" : "3.2.7", - "$client_language" : "en-US" - } - } - return self.client.track("$update_password", update_password_properties) + def transaction(self) -> sift.client.Response: + # Sample $transaction event + transaction_properties = { + # Required Fields + "$user_id": self.user_id, + "$amount": 506790000, # $506.79 + "$currency_code": "USD", + # Supported Fields + "$user_email": self.user_email, + "$verification_phone_number": "+123456789012", + "$transaction_type": "$sale", + "$transaction_status": "$failure", + "$decline_category": "$bank_decline", + "$order_id": "ORDER-123124124", + "$transaction_id": "719637215", + "$billing_address": { # or "$sent_address" # or "$received_address" + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$brand_name": "sift", + "$site_domain": "sift.com", + "$site_country": "US", + "$ordered_from": { + "$store_id": "123", + "$store_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6040", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + }, + # Credit card example + "$payment_method": { + "$payment_type": "$credit_card", + "$payment_gateway": "$braintree", + "$card_bin": "542486", + "$card_last4": "4444", + }, + # Supported fields for 3DS + "$status_3ds": "$attempted", + "$triggered_3ds": "$processor", + "$merchant_initiated_transaction": False, + # Supported Fields + "$shipping_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$session_id": "gigtleqddo84l8cm15qe4il", + # For marketplaces, use $seller_user_id to identify the seller + "$seller_user_id": "slinkys_emporium", + # Sample Custom Fields + "digital_wallet": "apple_pay", # "google_wallet", etc. + "coupon_code": "dollarMadness", + "shipping_choice": "FedEx Ground Courier", + "is_first_time_buyer": False, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } + return self.client.track("$transaction", transaction_properties) + + def update_account(self) -> sift.client.Response: + # Sample $update_account event + update_account_properties = { + # Required Fields + "$user_id": self.user_id, + # Supported Fields + "$changed_password": True, + "$user_email": self.user_email, + "$verification_phone_number": "+123456789012", + "$name": "Bill Jones", + "$phone": "1-415-555-6040", + "$referrer_user_id": "janejane102", + "$payment_methods": [ + { + "$payment_type": "$credit_card", + "$card_bin": "542486", + "$card_last4": "4444", + } + ], + "$billing_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$shipping_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$social_sign_on_type": "$twitter", + "$account_types": ["merchant", "premium"], + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } - def verification(self): - # Sample $verification event - verification_properties = { - # Required Fields - "$user_id" : self.user_id, - "$session_id" : "gigtleqddo84l8cm15qe4il", - "$status" : "$pending", + return self.client.track("$update_account", update_account_properties) + + def update_content_comment(self) -> sift.client.Response: + # Sample $update_content event for comments + update_content_comment_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "comment-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $comment object + "$comment": { + "$body": "Congrats on the new role!", + "$contact_email": "alex_301@domain.com", + "$parent_comment_id": "comment-23407", + "$root_content_id": "listing-12923213", + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "An old picture", + } + ], + }, + # Send this information from an APP client. + "$app": { + # Example for the iOS Calculator app. + "$os": "iOS", + "$os_version": "10.1.3", + "$device_manufacturer": "Apple", + "$device_model": "iPhone 4,2", + "$device_unique_id": "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", + "$app_name": "Calculator", + "$app_version": "3.2.7", + "$client_language": "en-US", + }, + } + return self.client.track( + "$update_content", update_content_comment_properties + ) + + def update_content_listing(self) -> sift.client.Response: + # Sample $update_content event for listings + update_content_listing_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "listing-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $listing object + "$listing": { + "$subject": "2 Bedroom Apartment for Rent", + "$body": "Capitol Hill Seattle brand new condo. 2 bedrooms and 1 full bath.", + "$contact_email": "alex_301@domain.com", + "$contact_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$locations": [ + { + "$city": "Seattle", + "$region": "Washington", + "$country": "US", + "$zipcode": "98112", + } + ], + "$listed_items": [ + { + "$price": 2950000000, # $2950.00 + "$currency_code": "USD", + "$tags": ["heat", "washer/dryer"], + } + ], + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "Billy's picture", + } + ], + "$expiration_time": 1549063157000, # UNIX timestamp in milliseconds + }, + # Send this information from an APP client. + "$app": { + # Example for the iOS Calculator app. + "$os": "iOS", + "$os_version": "10.1.3", + "$device_manufacturer": "Apple", + "$device_model": "iPhone 4,2", + "$device_unique_id": "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", + "$app_name": "Calculator", + "$app_version": "3.2.7", + "$client_language": "en-US", + }, + } + return self.client.track( + "$update_content", update_content_listing_properties + ) + + def update_content_message(self) -> sift.client.Response: + # Sample $update_content event for messages + update_content_message_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "message-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $message object + "$message": { + "$body": "Lets meet at 5pm", + "$contact_email": "alex_301@domain.com", + "$recipient_user_ids": ["fy9h989sjphh71"], + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "My hike today!", + } + ], + }, + # Send this information from a BROWSER client. + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + } - # Optional fields if applicable - "$verified_event" : "$login", - "$reason" : "$automated_rule", - "$verification_type" : "$sms", - "$verified_value" : "14155551212" - } - return self.client.track("$verification", verification_properties) - \ No newline at end of file + return self.client.track( + "$update_content", update_content_message_properties + ) + + def update_content_post(self) -> sift.client.Response: + # Sample $update_content event for posts + update_content_post_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "post-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $post object + "$post": { + "$subject": "My new apartment!", + "$body": "Moved into my new apartment yesterday.", + "$contact_email": "alex_301@domain.com", + "$contact_address": { + "$name": "Bill Jones", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$locations": [ + { + "$city": "Seattle", + "$region": "Washington", + "$country": "US", + "$zipcode": "98112", + } + ], + "$categories": ["Personal"], + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "View from the window!", + } + ], + "$expiration_time": 1549063157000, # UNIX timestamp in milliseconds + }, + # Send this information from an APP client. + "$app": { + # Example for the iOS Calculator app. + "$os": "iOS", + "$os_version": "10.1.3", + "$device_manufacturer": "Apple", + "$device_model": "iPhone 4,2", + "$device_unique_id": "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", + "$app_name": "Calculator", + "$app_version": "3.2.7", + "$client_language": "en-US", + }, + } + return self.client.track( + "$update_content", update_content_post_properties + ) + + def update_content_profile(self) -> sift.client.Response: + # Sample $update_content event for reviews + update_content_profile_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "profile-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $profile object + "$profile": { + "$body": "Hi! My name is Alex and I just moved to New London!", + "$contact_email": "alex_301@domain.com", + "$contact_address": { + "$name": "Alex Smith", + "$phone": "1-415-555-6041", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "Alex's picture", + } + ], + "$categories": ["Friends", "Long-term dating"], + }, + # ========================================= + # Send this information from an APP client. + "$app": { + # Example for the iOS Calculator app. + "$os": "iOS", + "$os_version": "10.1.3", + "$device_manufacturer": "Apple", + "$device_model": "iPhone 4,2", + "$device_unique_id": "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", + "$app_name": "Calculator", + "$app_version": "3.2.7", + "$client_language": "en-US", + }, + } + return self.client.track( + "$update_content", update_content_profile_properties + ) + + def update_content_review(self) -> sift.client.Response: + # Sample $update_content event for reviews + update_content_review_properties = { + # Required fields + "$user_id": self.user_id, + "$content_id": "review-23412", + # Recommended fields + "$session_id": "a234ksjfgn435sfg", + "$status": "$active", + "$ip": "255.255.255.0", + # Required $review object + "$review": { + "$subject": "Amazing Tacos!", + "$body": "I ate the tacos.", + "$contact_email": "alex_301@domain.com", + "$locations": [ + { + "$city": "Seattle", + "$region": "Washington", + "$country": "US", + "$zipcode": "98112", + } + ], + "$reviewed_content_id": "listing-234234", + "$images": [ + { + "$md5_hash": "0cc175b9c0f1b6a831c399e269772661", + "$link": "https://www.domain.com/file.png", + "$description": "Calamari tacos.", + } + ], + "$rating": 4.5, + }, + # Send this information from an APP client. + "$app": { + # Example for the iOS Calculator app. + "$os": "iOS", + "$os_version": "10.1.3", + "$device_manufacturer": "Apple", + "$device_model": "iPhone 4,2", + "$device_unique_id": "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", + "$app_name": "Calculator", + "$app_version": "3.2.7", + "$client_language": "en-US", + }, + } + return self.client.track( + "$update_content", update_content_review_properties + ) + + def update_order(self) -> sift.client.Response: + # Sample $update_order event + update_order_properties = { + # Required Fields + "$user_id": self.user_id, + # Supported Fields + "$session_id": "gigtleqddo84l8cm15qe4il", + "$order_id": "ORDER-28168441", + "$user_email": self.user_email, + "$verification_phone_number": "+123456789012", + "$amount": 115940000, # $115.94 + "$currency_code": "USD", + "$billing_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$payment_methods": [ + { + "$payment_type": "$credit_card", + "$payment_gateway": "$braintree", + "$card_bin": "542486", + "$card_last4": "4444", + } + ], + "$brand_name": "sift", + "$site_domain": "sift.com", + "$site_country": "US", + "$ordered_from": { + "$store_id": "123", + "$store_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6040", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + }, + "$shipping_address": { + "$name": "Bill Jones", + "$phone": "1-415-555-6041", + "$address_1": "2100 Main Street", + "$address_2": "Apt 3B", + "$city": "New London", + "$region": "New Hampshire", + "$country": "US", + "$zipcode": "03257", + }, + "$expedited_shipping": True, + "$shipping_method": "$physical", + "$shipping_carrier": "UPS", + "$shipping_tracking_numbers": [ + "1Z204E380338943508", + "1Z204E380338943509", + ], + "$items": [ + { + "$item_id": "12344321", + "$product_title": "Microwavable Kettle Corn: Original Flavor", + "$price": 4990000, # $4.99 + "$upc": "097564307560", + "$sku": "03586005", + "$brand": "Peters Kettle Corn", + "$manufacturer": "Peters Kettle Corn", + "$category": "Food and Grocery", + "$tags": ["Popcorn", "Snacks", "On Sale"], + "$quantity": 4, + }, + { + "$item_id": "B004834GQO", + "$product_title": "The Slanket Blanket-Texas Tea", + "$price": 39990000, # $39.99 + "$upc": "6786211451001", + "$sku": "004834GQ", + "$brand": "Slanket", + "$manufacturer": "Slanket", + "$category": "Blankets & Throws", + "$tags": ["Awesome", "Wintertime specials"], + "$color": "Texas Tea", + "$quantity": 2, + }, + ], + # For marketplaces, use $seller_user_id to identify the seller + "$seller_user_id": "slinkys_emporium", + "$promotions": [ + { + "$promotion_id": "FirstTimeBuyer", + "$status": "$success", + "$description": "$5 off", + "$discount": { + "$amount": 5000000, # $5.00 + "$currency_code": "USD", + "$minimum_purchase_amount": 25000000, # $25.00 + }, + } + ], + # Sample Custom Fields + "digital_wallet": "apple_pay", # "google_wallet", etc. + "coupon_code": "dollarMadness", + "shipping_choice": "FedEx Ground Courier", + "is_first_time_buyer": False, + # Send this information from an APP client. + "$app": { + # Example for the iOS Calculator app. + "$os": "iOS", + "$os_version": "10.1.3", + "$device_manufacturer": "Apple", + "$device_model": "iPhone 4,2", + "$device_unique_id": "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", + "$app_name": "Calculator", + "$app_version": "3.2.7", + "$client_language": "en-US", + }, + } + return self.client.track("$update_order", update_order_properties) + + def update_password(self) -> sift.client.Response: + # Sample $update_password event + update_password_properties = { + # Required Fields + "$user_id": self.user_id, + "$session_id": "gigtleqddo84l8cm15qe4il", + "$status": "$success", + "$reason": "$forced_reset", + "$ip": "128.148.1.135", # IP of the user that entered the new password after the old password was reset + # Send this information from an APP client. + "$app": { + # Example for the iOS Calculator app. + "$os": "iOS", + "$os_version": "10.1.3", + "$device_manufacturer": "Apple", + "$device_model": "iPhone 4,2", + "$device_unique_id": "A3D261E4-DE0A-470B-9E4A-720F3D3D22E6", + "$app_name": "Calculator", + "$app_version": "3.2.7", + "$client_language": "en-US", + }, + } + return self.client.track( + "$update_password", update_password_properties + ) + + def verification(self) -> sift.client.Response: + # Sample $verification event + verification_properties = { + # Required Fields + "$user_id": self.user_id, + "$session_id": "gigtleqddo84l8cm15qe4il", + "$status": "$pending", + # Optional fields if applicable + "$verified_event": "$login", + "$reason": "$automated_rule", + "$verification_type": "$sms", + "$verified_value": "14155551212", + } + return self.client.track("$verification", verification_properties) diff --git a/test_integration_app/globals.py b/test_integration_app/globals.py index 030531e..cc7959e 100644 --- a/test_integration_app/globals.py +++ b/test_integration_app/globals.py @@ -1,5 +1,7 @@ -def initialize(): +user_id = "billy_jones_301" +user_email = "billjones1@example.com" +session_id = "gigtleqddo84l8cm15qe4il" + + +def initialize() -> None: global user_id, user_email, session_id - user_id = 'billy_jones_301' - user_email = 'billjones1@example.com' - session_id = 'gigtleqddo84l8cm15qe4il' diff --git a/test_integration_app/main.py b/test_integration_app/main.py index 63fd928..6ac0fb8 100644 --- a/test_integration_app/main.py +++ b/test_integration_app/main.py @@ -1,113 +1,129 @@ -import string import random +import string -from events_api import test_events_api from decisions_api import test_decisions_api -from workflows_api import test_workflows_api +from events_api import test_events_api +from psp_merchant_api import test_psp_merchant_api from score_api import test_score_api from verifications_api import test_verification_api -from psp_merchant_api import test_psp_merchant_api +from workflows_api import test_workflows_api + +from sift.client import Response -class Utils: - def isOK(self, response): - if(hasattr(response, 'status')): - return ((response.status == 0) and ((response.http_status_code == 200) or (response.http_status_code == 201))) - else: - return ((response.http_status_code == 200) or (response.http_status_code == 201)) - - def is_ok_with_warnings(self, response): - return self.isOK(response) and \ - hasattr(response, 'body') and \ - len(response.body['warnings']) > 0 - - def is_ok_without_warnings(self, response): - return self.isOK(response) and \ - hasattr(response, 'body') and \ - 'warnings' not in response.body - -def runAllMethods(): - objUtils = Utils() - objEvents = test_events_api.EventsAPI() - objDecision = test_decisions_api.DecisionAPI() - objScore = test_score_api.ScoreAPI() - objWorkflow = test_workflows_api.WorkflowsAPI() - objVerification = test_verification_api.VerificationAPI() - objPSPMerchant = test_psp_merchant_api.PSPMerchantAPI() + +def is_ok(response: Response) -> bool: + if hasattr(response, "status"): + return response.status == 0 and response.http_status_code in (200, 201) + + return response.http_status_code in (200, 201) + + +def is_ok_with_warnings(response: Response) -> bool: + return ( + is_ok(response) + and hasattr(response, "body") + and isinstance(response.body, dict) + and bool(response.body["warnings"]) + ) + + +def is_ok_without_warnings(response: Response) -> bool: + return ( + is_ok(response) + and hasattr(response, "body") + and isinstance(response.body, dict) + and "warnings" not in response.body + ) + + +def run_all_methods() -> None: + obj_events = test_events_api.EventsAPI() + obj_decisions = test_decisions_api.DecisionAPI() + obj_score = test_score_api.ScoreAPI() + obj_workflow = test_workflows_api.WorkflowsAPI() + obj_verification = test_verification_api.VerificationAPI() + obj_psp_merchant = test_psp_merchant_api.PSPMerchantAPI() # Events APIs - assert (objUtils.isOK(objEvents.add_item_to_cart()) == True) - assert (objUtils.isOK(objEvents.add_promotion()) == True) - assert (objUtils.isOK(objEvents.chargeback()) == True) - assert (objUtils.isOK(objEvents.content_status()) == True) - assert (objUtils.isOK(objEvents.create_account()) == True) - assert (objUtils.isOK(objEvents.create_content_comment()) == True) - assert (objUtils.isOK(objEvents.create_content_listing()) == True) - assert (objUtils.isOK(objEvents.create_content_message()) == True) - assert (objUtils.isOK(objEvents.create_content_post()) == True) - assert (objUtils.isOK(objEvents.create_content_profile()) == True) - assert (objUtils.isOK(objEvents.create_content_review()) == True) - assert (objUtils.isOK(objEvents.create_order()) == True) - assert (objUtils.isOK(objEvents.flag_content()) == True) - assert (objUtils.isOK(objEvents.link_session_to_user()) == True) - assert (objUtils.isOK(objEvents.login()) == True) - assert (objUtils.isOK(objEvents.logout()) == True) - assert (objUtils.isOK(objEvents.order_status()) == True) - assert (objUtils.isOK(objEvents.remove_item_from_cart()) == True) - assert (objUtils.isOK(objEvents.security_notification()) == True) - assert (objUtils.isOK(objEvents.transaction()) == True) - assert (objUtils.isOK(objEvents.update_account()) == True) - assert (objUtils.isOK(objEvents.update_content_comment()) == True) - assert (objUtils.isOK(objEvents.update_content_listing()) == True) - assert (objUtils.isOK(objEvents.update_content_message()) == True) - assert (objUtils.isOK(objEvents.update_content_post()) == True) - assert (objUtils.isOK(objEvents.update_content_profile()) == True) - assert (objUtils.isOK(objEvents.update_content_review()) == True) - assert (objUtils.isOK(objEvents.update_order()) == True) - assert (objUtils.isOK(objEvents.update_password()) == True) - assert (objUtils.isOK(objEvents.verification()) == True) + assert is_ok(obj_events.add_item_to_cart()) + assert is_ok(obj_events.add_promotion()) + assert is_ok(obj_events.chargeback()) + assert is_ok(obj_events.content_status()) + assert is_ok(obj_events.create_account()) + assert is_ok(obj_events.create_content_comment()) + assert is_ok(obj_events.create_content_listing()) + assert is_ok(obj_events.create_content_message()) + assert is_ok(obj_events.create_content_post()) + assert is_ok(obj_events.create_content_profile()) + assert is_ok(obj_events.create_content_review()) + assert is_ok(obj_events.create_order()) + assert is_ok(obj_events.flag_content()) + assert is_ok(obj_events.link_session_to_user()) + assert is_ok(obj_events.login()) + assert is_ok(obj_events.logout()) + assert is_ok(obj_events.order_status()) + assert is_ok(obj_events.remove_item_from_cart()) + assert is_ok(obj_events.security_notification()) + assert is_ok(obj_events.transaction()) + assert is_ok(obj_events.update_account()) + assert is_ok(obj_events.update_content_comment()) + assert is_ok(obj_events.update_content_listing()) + assert is_ok(obj_events.update_content_message()) + assert is_ok(obj_events.update_content_post()) + assert is_ok(obj_events.update_content_profile()) + assert is_ok(obj_events.update_content_review()) + assert is_ok(obj_events.update_order()) + assert is_ok(obj_events.update_password()) + assert is_ok(obj_events.verification()) # Testing include warnings query param - assert (objUtils.is_ok_without_warnings(objEvents.create_order()) == True) - assert (objUtils.is_ok_with_warnings(objEvents.create_order_with_warnings()) == True) + assert is_ok_without_warnings(obj_events.create_order()) + assert is_ok_with_warnings(obj_events.create_order_with_warnings()) print("Events API Tested") # Decision APIs - assert (objUtils.isOK(objDecision.apply_user_decision()) == True) - assert (objUtils.isOK(objDecision.apply_order_decision()) == True) - assert (objUtils.isOK(objDecision.apply_session_decision()) == True) - assert (objUtils.isOK(objDecision.apply_content_decision()) == True) - assert (objUtils.isOK(objDecision.get_user_decisions()) == True) - assert (objUtils.isOK(objDecision.get_order_decisions()) == True) - assert (objUtils.isOK(objDecision.get_content_decisions()) == True) - assert (objUtils.isOK(objDecision.get_session_decisions()) == True) - assert (objUtils.isOK(objDecision.get_decisions()) == True) + assert is_ok(obj_decisions.apply_user_decision()) + assert is_ok(obj_decisions.apply_order_decision()) + assert is_ok(obj_decisions.apply_session_decision()) + assert is_ok(obj_decisions.apply_content_decision()) + assert is_ok(obj_decisions.get_user_decisions()) + assert is_ok(obj_decisions.get_order_decisions()) + assert is_ok(obj_decisions.get_content_decisions()) + assert is_ok(obj_decisions.get_session_decisions()) + assert is_ok(obj_decisions.get_decisions()) print("Decision API Tested") # Workflows APIs - assert (objUtils.isOK(objWorkflow.synchronous_workflows()) == True) + assert is_ok(obj_workflow.synchronous_workflows()) print("Workflow API Tested") # Score APIs - assert (objUtils.isOK(objScore.get_user_score()) == True) + assert is_ok(obj_score.get_user_score()) print("Score API Tested") # Verification APIs - assert (objUtils.isOK(objVerification.send()) == True) - assert (objUtils.isOK(objVerification.resend()) == True) - checkResponse = objVerification.check() - assert (objUtils.isOK(checkResponse) == True) - assert (checkResponse.body["status"] == 50) + assert is_ok(obj_verification.send()) + assert is_ok(obj_verification.resend()) + checkResponse = obj_verification.check() + assert is_ok(checkResponse) + assert isinstance(checkResponse.body, dict) + assert checkResponse.body["status"] == 50 print("Verification API Tested") # PSP Merchant APIs - merchant_id = 'merchant_id_test_app' + ''.join(random.choices(string.digits, k = 7)) - assert (objUtils.isOK(objPSPMerchant.create_merchant(merchant_id)) == True) - assert (objUtils.isOK(objPSPMerchant.edit_merchant(merchant_id)) == True) - assert (objUtils.isOK(objPSPMerchant.get_merchant_profiles()) == True) - assert (objUtils.isOK(objPSPMerchant.get_merchant_profiles(batch_size=10, batch_token=None)) == True) + merchant_id = "merchant_id_test_app" + "".join( + random.choices(string.digits, k=7) + ) + assert is_ok(obj_psp_merchant.create_merchant(merchant_id)) + assert is_ok(obj_psp_merchant.edit_merchant(merchant_id)) + assert is_ok(obj_psp_merchant.get_merchant_profiles()) + assert is_ok( + obj_psp_merchant.get_merchant_profiles(batch_size=10, batch_token=None) + ) print("PSP Merchant API Tested") print("API Integration tests execution finished") -runAllMethods() + +run_all_methods() diff --git a/test_integration_app/psp_merchant_api/test_psp_merchant_api.py b/test_integration_app/psp_merchant_api/test_psp_merchant_api.py index 3d44114..6a023d2 100644 --- a/test_integration_app/psp_merchant_api/test_psp_merchant_api.py +++ b/test_integration_app/psp_merchant_api/test_psp_merchant_api.py @@ -1,69 +1,67 @@ -import sift -import string -import random # define the random module +from __future__ import annotations from os import environ as env -class PSPMerchantAPI(): - # Get the value of API_KEY and ACCOUNT_ID from environment variable - api_key = env['API_KEY'] - account_id = env['ACCOUNT_ID'] +import sift + + +class PSPMerchantAPI: + # Get the value of API_KEY and ACCOUNT_ID from environment variable + api_key = env["API_KEY"] + account_id = env["ACCOUNT_ID"] + + client = sift.Client(api_key=api_key, account_id=account_id) + + def create_merchant(self, merchant_id: str) -> sift.client.Response: + properties = { + "id": merchant_id, + "name": "Wonderful Payments Inc.13", + "description": "Wonderful Payments payment provider.", + "address": { + "name": "Alany", + "address_1": "Big Payment blvd, 22", + "address_2": "apt, 8", + "city": "New Orleans", + "region": "NA", + "country": "US", + "zipcode": "76830", + "phone": "0394888320", + }, + "category": "1002", + "service_level": "Platinum", + "status": "active", + "risk_profile": {"level": "low", "score": 10}, + } + return self.client.create_psp_merchant_profile(properties) - client = sift.Client(api_key = api_key, account_id = account_id) - - def create_merchant(self, merchant_id): - merchantProperties={ - "id": merchant_id, - "name": "Wonderful Payments Inc.13", - "description": "Wonderful Payments payment provider.", - "address": { - "name": "Alany", - "address_1": "Big Payment blvd, 22", - "address_2": "apt, 8", - "city": "New Orleans", - "region": "NA", - "country": "US", - "zipcode": "76830", - "phone": "0394888320" - }, - "category": "1002", - "service_level": "Platinum", - "status": "active", - "risk_profile": { - "level": "low", - "score": 10 - } - } - return self.client.create_psp_merchant_profile(merchantProperties) - - def edit_merchant(self, merchant_id): - merchantProperties = { - "id": merchant_id, - "name": "Wonderful Payments Inc.13 edit", - "description": "Wonderful Payments payment provider. edit", - "address": { - "name": "Alany", - "address_1": "Big Payment blvd, 22", - "address_2": "apt, 8", - "city": "New Orleans", - "region": "NA", - "country": "US", - "zipcode": "76830", - "phone": "0394888320" - }, - "category": "1002", - "service_level": "Platinum", - "status": "active", - "risk_profile": { - "level": "low", - "score": 10 - } - } - return self.client.update_psp_merchant_profile(merchant_id, merchantProperties) + def edit_merchant(self, merchant_id: str) -> sift.client.Response: + properties = { + "id": merchant_id, + "name": "Wonderful Payments Inc.13 edit", + "description": "Wonderful Payments payment provider. edit", + "address": { + "name": "Alany", + "address_1": "Big Payment blvd, 22", + "address_2": "apt, 8", + "city": "New Orleans", + "region": "NA", + "country": "US", + "zipcode": "76830", + "phone": "0394888320", + }, + "category": "1002", + "service_level": "Platinum", + "status": "active", + "risk_profile": {"level": "low", "score": 10}, + } + return self.client.update_psp_merchant_profile(merchant_id, properties) - def get_a_merchant_profile(self, merchant_id): - return self.client.get_a_psp_merchant_profile(merchant_id) + def get_a_merchant_profile(self, merchant_id: str) -> sift.client.Response: + return self.client.get_a_psp_merchant_profile(merchant_id) - def get_merchant_profiles(self, batch_token = None, batch_size = None): - return self.client.get_psp_merchant_profiles(batch_token, batch_size) - \ No newline at end of file + def get_merchant_profiles( + self, + batch_token: str | None = None, + batch_size: int | None = None, + ) -> sift.client.Response: + return self.client.get_psp_merchant_profiles(batch_token, batch_size) diff --git a/test_integration_app/score_api/test_score_api.py b/test_integration_app/score_api/test_score_api.py index 5c61b22..844fdaa 100644 --- a/test_integration_app/score_api/test_score_api.py +++ b/test_integration_app/score_api/test_score_api.py @@ -1,14 +1,19 @@ -import sift +from os import environ as env + import globals -from os import environ as env +import sift + + +class ScoreAPI: + # Get the value of API_KEY from environment variable + api_key = env["API_KEY"] + client = sift.Client(api_key=api_key) + globals.initialize() + user_id = globals.user_id -class ScoreAPI(): - # Get the value of API_KEY from environment variable - api_key = env['API_KEY'] - client = sift.Client(api_key = api_key) - globals.initialize() - user_id = globals.user_id - - def get_user_score(self): - return self.client.get_user_score(user_id = self.user_id, abuse_types=["payment_abuse", "promotion_abuse"]) + def get_user_score(self) -> sift.client.Response: + return self.client.get_user_score( + user_id=self.user_id, + abuse_types=["payment_abuse", "promotion_abuse"], + ) diff --git a/test_integration_app/verifications_api/test_verification_api.py b/test_integration_app/verifications_api/test_verification_api.py index b35478d..899df21 100644 --- a/test_integration_app/verifications_api/test_verification_api.py +++ b/test_integration_app/verifications_api/test_verification_api.py @@ -1,52 +1,54 @@ -import sift +from os import environ as env + import globals -from os import environ as env +import sift -class VerificationAPI(): + +class VerificationAPI: # Get the value of API_KEY from environment variable - api_key = env['API_KEY'] - client = sift.Client(api_key = api_key) + api_key = env["API_KEY"] + client = sift.Client(api_key=api_key) globals.initialize() user_id = globals.user_id user_email = globals.user_email - - def send(self): - sendProperties = { - '$user_id': self.user_id, - '$send_to': self.user_email, - '$verification_type': '$email', - '$brand_name': 'MyTopBrand', - '$language': 'en', - '$site_country': 'IN', - '$event': { - '$session_id': 'SOME_SESSION_ID', - '$verified_event': '$login', - '$verified_entity_id': 'SOME_SESSION_ID', - '$reason': '$automated_rule', - '$ip': '192.168.1.1', - '$browser': { - '$user_agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36', - '$accept_language': 'en-US', - '$content_language': 'en-GB' - } - } - } - return self.client.verification_send(sendProperties) - - def resend(self): - resendProperties = { - '$user_id': self.user_id, - '$verified_event': '$login', - '$verified_entity_id': 'SOME_SESSION_ID' - } - return self.client.verification_resend(resendProperties) - def check(self): - checkProperties = { - '$user_id': self.user_id, - '$code': '123456', - '$verified_event': '$login', - '$verified_entity_id': "SOME_SESSION_ID" + def send(self) -> sift.client.Response: + properties = { + "$user_id": self.user_id, + "$send_to": self.user_email, + "$verification_type": "$email", + "$brand_name": "MyTopBrand", + "$language": "en", + "$site_country": "IN", + "$event": { + "$session_id": "SOME_SESSION_ID", + "$verified_event": "$login", + "$verified_entity_id": "SOME_SESSION_ID", + "$reason": "$automated_rule", + "$ip": "192.168.1.1", + "$browser": { + "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36", + "$accept_language": "en-US", + "$content_language": "en-GB", + }, + }, + } + return self.client.verification_send(properties) + + def resend(self) -> sift.client.Response: + properties = { + "$user_id": self.user_id, + "$verified_event": "$login", + "$verified_entity_id": "SOME_SESSION_ID", + } + return self.client.verification_resend(properties) + + def check(self) -> sift.client.Response: + properties = { + "$user_id": self.user_id, + "$code": "123456", + "$verified_event": "$login", + "$verified_entity_id": "SOME_SESSION_ID", } - return self.client.verification_check(checkProperties) + return self.client.verification_check(properties) diff --git a/test_integration_app/workflows_api/test_workflows_api.py b/test_integration_app/workflows_api/test_workflows_api.py index 4dd26d6..afb55f5 100644 --- a/test_integration_app/workflows_api/test_workflows_api.py +++ b/test_integration_app/workflows_api/test_workflows_api.py @@ -1,20 +1,28 @@ -import sift -import globals from os import environ as env -class WorkflowsAPI(): +import globals + +import sift + + +class WorkflowsAPI: # Get the value of API_KEY from environment variable - api_key = env['API_KEY'] - client = sift.Client(api_key = api_key) + api_key = env["API_KEY"] + client = sift.Client(api_key=api_key) globals.initialize() user_id = globals.user_id user_email = globals.user_email - - def synchronous_workflows(self): + + def synchronous_workflows(self) -> sift.client.Response: properties = { - '$user_id' : self.user_id, - '$user_email' : self.user_email - } - return self.client.track('$create_order', properties, return_workflow_status=True, - return_route_info=True, abuse_types=['promo_abuse', 'content_abuse', 'payment_abuse']) - \ No newline at end of file + "$user_id": self.user_id, + "$user_email": self.user_email, + } + + return self.client.track( + "$create_order", + properties, + return_workflow_status=True, + return_route_info=True, + abuse_types=["promo_abuse", "content_abuse", "payment_abuse"], + ) diff --git a/tests/test_client.py b/tests/test_client.py index cc6a6b4..fb38a53 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -1,53 +1,50 @@ +from __future__ import annotations + import datetime import json -import sys -import unittest +import typing as t import warnings from decimal import Decimal -from requests.auth import HTTPBasicAuth +from unittest import TestCase, mock -import mock -import requests.exceptions +from requests.auth import HTTPBasicAuth +from requests.exceptions import RequestException import sift - -if sys.version_info[0] < 3: - import six.moves.urllib as urllib -else: - import urllib.parse +from sift.utils import quote_path as _q -def valid_transaction_properties(): +def valid_transaction_properties() -> dict[str, t.Any]: return { - '$buyer_user_id': '123456', - '$seller_user_id': '654321', - '$amount': Decimal('1253200.0'), - '$currency_code': 'USD', - '$time': int(datetime.datetime.now().strftime('%S')), - '$transaction_id': 'my_transaction_id', - '$billing_name': 'Mike Snow', - '$billing_bin': '411111', - '$billing_last4': '1111', - '$billing_address1': '123 Main St.', - '$billing_city': 'San Francisco', - '$billing_region': 'CA', - '$billing_country': 'US', - '$billing_zip': '94131', - '$user_email': 'mike@example.com' + "$buyer_user_id": "123456", + "$seller_user_id": "654321", + "$amount": Decimal("1253200.0"), + "$currency_code": "USD", + "$time": int(datetime.datetime.now().strftime("%S")), + "$transaction_id": "my_transaction_id", + "$billing_name": "Mike Snow", + "$billing_bin": "411111", + "$billing_last4": "1111", + "$billing_address1": "123 Main St.", + "$billing_city": "San Francisco", + "$billing_region": "CA", + "$billing_country": "US", + "$billing_zip": "94131", + "$user_email": "mike@example.com", } -def valid_label_properties(): +def valid_label_properties() -> dict[str, t.Any]: return { - '$abuse_type': 'content_abuse', - '$is_bad': True, - '$description': 'Listed a fake item', - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$abuse_type": "content_abuse", + "$is_bad": True, + "$description": "Listed a fake item", + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", } -def valid_psp_merchant_properties(): +def valid_psp_merchant_properties() -> dict[str, t.Any]: return { "$id": "api-key-1", "$name": "Wonderful Payments Inc.", @@ -65,14 +62,11 @@ def valid_psp_merchant_properties(): "$category": "1002", "$service_level": "Platinum", "$status": "active", - "$risk_profile": { - "$level": "low", - "$score": 10 - } + "$risk_profile": {"$level": "low", "$score": 10}, } -def valid_psp_merchant_properties_response(): +def valid_psp_merchant_properties_response() -> str: return """{ "id":"api-key-1", "name": "Wonderful Payments Inc.", @@ -97,7 +91,7 @@ def valid_psp_merchant_properties_response(): }""" -def score_response_json(): +def score_response_json() -> str: return """{ "status": 0, "error_message": "OK", @@ -128,7 +122,7 @@ def score_response_json(): }""" -def workflow_statuses_json(): +def workflow_statuses_json() -> str: return """{ "route" : { "name" : "my route" @@ -182,7 +176,7 @@ def workflow_statuses_json(): }""" -def action_response_json(): +def action_response_json() -> str: return """{ "actions": [ { @@ -229,20 +223,21 @@ def action_response_json(): }""" -def response_with_data_header(): - return { - 'content-type': 'application/json; charset=UTF-8' - } +def response_with_data_header() -> dict[str, t.Any]: + return {"content-type": "application/json; charset=UTF-8"} -class TestSiftPythonClient(unittest.TestCase): +class TestSiftPythonClient(TestCase): - def setUp(self): - self.test_key = 'a_fake_test_api_key' - self.account_id = 'ACCT' - self.sift_client = sift.Client(api_key=self.test_key, account_id=self.account_id) + def setUp(self) -> None: + self.test_key = "a_fake_test_api_key" + self.account_id = "ACCT" + self.sift_client = sift.Client( + api_key=self.test_key, + account_id=self.account_id, + ) - def test_global_api_key(self): + def test_global_api_key(self) -> None: # test for error if global key is undefined self.assertRaises(TypeError, sift.Client) sift.api_key = "a_test_global_api_key" @@ -252,133 +247,156 @@ def test_global_api_key(self): client2 = sift.Client(local_api_key) # test that global api key is assigned - assert (client1.api_key == sift.api_key) + assert client1.api_key == sift.api_key # test that local api key is assigned - assert (client2.api_key == local_api_key) + assert client2.api_key == local_api_key client2 = sift.Client() # test that client2 is assigned a new object with global api_key - assert (client2.api_key == sift.api_key) + assert client2.api_key == sift.api_key - def test_constructor_requires_valid_api_key(self): + def test_constructor_requires_valid_api_key(self) -> None: self.assertRaises(TypeError, sift.Client, None) - self.assertRaises(ValueError, sift.Client, '') + self.assertRaises(ValueError, sift.Client, "") - def test_constructor_invalid_api_url(self): + def test_constructor_invalid_api_url(self) -> None: self.assertRaises(TypeError, sift.Client, self.test_key, None) - self.assertRaises(ValueError, sift.Client, self.test_key, '') + self.assertRaises(ValueError, sift.Client, self.test_key, "") - def test_constructor_api_key(self): + def test_constructor_api_key(self) -> None: client = sift.Client(self.test_key) self.assertEqual(client.api_key, self.test_key) - def test_track_requires_valid_event(self): + def test_track_requires_valid_event(self) -> None: self.assertRaises(TypeError, self.sift_client.track, None, {}) - self.assertRaises(ValueError, self.sift_client.track, '', {}) + self.assertRaises(ValueError, self.sift_client.track, "", {}) self.assertRaises(TypeError, self.sift_client.track, 42, {}) - def test_track_requires_properties(self): - event = 'custom_event' + def test_track_requires_properties(self) -> None: + event = "custom_event" self.assertRaises(TypeError, self.sift_client.track, event, None) self.assertRaises(TypeError, self.sift_client.track, event, 42) self.assertRaises(ValueError, self.sift_client.track, event, {}) - def test_score_requires_user_id(self): + def test_score_requires_user_id(self) -> None: self.assertRaises(TypeError, self.sift_client.score, None) - self.assertRaises(ValueError, self.sift_client.score, '') + self.assertRaises(ValueError, self.sift_client.score, "") self.assertRaises(TypeError, self.sift_client.score, 42) - def test_event_ok(self): - event = '$transaction' + def test_event_ok(self) -> None: + event = "$transaction" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.track(event, valid_transaction_properties()) + + response = self.sift_client.track( + event, valid_transaction_properties() + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={}) + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_event_with_timeout_param_ok(self): - event = '$transaction' + def test_event_with_timeout_param_ok(self) -> None: + event = "$transaction" test_timeout = 5 mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.track( - event, valid_transaction_properties(), timeout=test_timeout) + event, valid_transaction_properties(), timeout=test_timeout + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=test_timeout, - params={}) + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_score_ok(self): + def test_score_ok(self) -> None: mock_response = mock.Mock() mock_response.content = score_response_json() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.score('12345') + + response = self.sift_client.score("12345") + mock_get.assert_called_with( - 'https://api.siftscience.com/v205/score/12345', + "https://api.sift.com/v205/score/12345", params={}, headers=mock.ANY, timeout=mock.ANY, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['score'] == 0.85) - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - - def test_score_with_timeout_param_ok(self): + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score"] == 0.85 + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + + def test_score_with_timeout_param_ok(self) -> None: test_timeout = 5 mock_response = mock.Mock() mock_response.content = score_response_json() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.score('12345', test_timeout) + + response = self.sift_client.score("12345", test_timeout) + mock_get.assert_called_with( - 'https://api.siftscience.com/v205/score/12345', + "https://api.sift.com/v205/score/12345", params={}, headers=mock.ANY, timeout=test_timeout, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['score'] == 0.85) - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - - def test_get_user_score_ok(self): - """Test the GET /{version}/users/{userId}/score API, i.e. client.get_user_score() + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score"] == 0.85 + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + + def test_get_user_score_ok(self) -> None: + """ + Test the GET /{version}/users/{userId}/score API, + i.e. client.get_user_score() """ test_timeout = 5 mock_response = mock.Mock() @@ -386,25 +404,32 @@ def test_get_user_score_ok(self): mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_user_score('12345', test_timeout) + + response = self.sift_client.get_user_score("12345", test_timeout) + mock_get.assert_called_with( - 'https://api.siftscience.com/v205/users/12345/score', + "https://api.sift.com/v205/users/12345/score", params={}, headers=mock.ANY, timeout=test_timeout, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['entity_id'] == '12345') - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - assert ('latest_decisions' in response.body) - - def test_get_user_score_with_abuse_types_ok(self): - """Test the GET /{version}/users/{userId}/score?abuse_types=... API, i.e. client.get_user_score() + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["entity_id"] == "12345" + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + assert "latest_decisions" in response.body + + def test_get_user_score_with_abuse_types_ok(self) -> None: + """ + Test the GET /{version}/users/{userId}/score?abuse_types=... API, + i.e. client.get_user_score() """ test_timeout = 5 mock_response = mock.Mock() @@ -412,27 +437,36 @@ def test_get_user_score_with_abuse_types_ok(self): mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_user_score('12345', - abuse_types=['payment_abuse', 'content_abuse'], - timeout=test_timeout) + + response = self.sift_client.get_user_score( + "12345", + abuse_types=["payment_abuse", "content_abuse"], + timeout=test_timeout, + ) + mock_get.assert_called_with( - 'https://api.siftscience.com/v205/users/12345/score', - params={'abuse_types': 'payment_abuse,content_abuse'}, + "https://api.sift.com/v205/users/12345/score", + params={"abuse_types": "payment_abuse,content_abuse"}, headers=mock.ANY, timeout=test_timeout, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['entity_id'] == '12345') - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - assert ('latest_decisions' in response.body) - - def test_rescore_user_ok(self): - """Test the POST /{version}/users/{userId}/score API, i.e. client.rescore_user() + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["entity_id"] == "12345" + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + assert "latest_decisions" in response.body + + def test_rescore_user_ok(self) -> None: + """ + Test the POST /{version}/users/{userId}/score API, + i.e. client.rescore_user() """ test_timeout = 5 mock_response = mock.Mock() @@ -440,25 +474,32 @@ def test_rescore_user_ok(self): mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.rescore_user('12345', test_timeout) + + response = self.sift_client.rescore_user("12345", test_timeout) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/users/12345/score', + "https://api.sift.com/v205/users/12345/score", params={}, headers=mock.ANY, timeout=test_timeout, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['entity_id'] == '12345') - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - assert ('latest_decisions' in response.body) - - def test_rescore_user_with_abuse_types_ok(self): - """Test the POST /{version}/users/{userId}/score?abuse_types=... API, i.e. client.rescore_user() + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["entity_id"] == "12345" + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + assert "latest_decisions" in response.body + + def test_rescore_user_with_abuse_types_ok(self) -> None: + """ + Test the POST /{version}/users/{userId}/score?abuse_types=... API, + i.e. client.rescore_user() """ test_timeout = 5 mock_response = mock.Mock() @@ -466,117 +507,126 @@ def test_rescore_user_with_abuse_types_ok(self): mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.rescore_user('12345', - abuse_types=['payment_abuse', 'content_abuse'], - timeout=test_timeout) + + response = self.sift_client.rescore_user( + "12345", + abuse_types=["payment_abuse", "content_abuse"], + timeout=test_timeout, + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/users/12345/score', - params={'abuse_types': 'payment_abuse,content_abuse'}, + "https://api.sift.com/v205/users/12345/score", + params={"abuse_types": "payment_abuse,content_abuse"}, headers=mock.ANY, timeout=test_timeout, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['entity_id'] == '12345') - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - assert ('latest_decisions' in response.body) - - def test_sync_score_ok(self): - event = '$transaction' + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["entity_id"] == "12345" + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + assert "latest_decisions" in response.body + + def test_sync_score_ok(self) -> None: + event = "$transaction" mock_response = mock.Mock() - mock_response.content = ('{"status": 0, "error_message": "OK", "score_response": %s}' - % score_response_json()) + mock_response.content = f'{{"status": 0, "error_message": "OK", "score_response": {score_response_json()}}}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.track( event, valid_transaction_properties(), return_score=True, - abuse_types=['payment_abuse', 'content_abuse', 'legacy']) - mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', - data=mock.ANY, - headers=mock.ANY, - timeout=mock.ANY, - params={'return_score': 'true', 'abuse_types': 'payment_abuse,content_abuse,legacy'}) - self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") - assert (response.body['score_response']['score'] == 0.85) - assert (response.body['score_response']['scores']['content_abuse']['score'] == 0.14) - assert (response.body['score_response']['scores']['payment_abuse']['score'] == 0.97) - - def test_sync_workflow_ok(self): - event = '$transaction' - mock_response = mock.Mock() - mock_response.content = ('{"status": 0, "error_message": "OK", "workflow_statuses": %s}' - % workflow_statuses_json()) - mock_response.json.return_value = json.loads(mock_response.content) - mock_response.status_code = 200 - mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: - mock_post.return_value = mock_response - response = self.sift_client.track( - event, - valid_transaction_properties(), - return_workflow_status=True, - return_route_info=True, - abuse_types=['payment_abuse', 'content_abuse', 'legacy']) + abuse_types=["payment_abuse", "content_abuse", "legacy"], + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'return_workflow_status': 'true', 'return_route_info': 'true', - 'abuse_types': 'payment_abuse,content_abuse,legacy'}) + params={ + "return_score": "true", + "abuse_types": "payment_abuse,content_abuse,legacy", + }, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") - assert (response.body['workflow_statuses']['route']['name'] == 'my route') + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score_response"]["score"] == 0.85 + assert ( + response.body["score_response"]["scores"]["content_abuse"][ + "score" + ] + == 0.14 + ) + assert ( + response.body["score_response"]["scores"]["payment_abuse"][ + "score" + ] + == 0.97 + ) - def test_sync_workflow_ok(self): - event = '$transaction' + def test_sync_workflow_ok(self) -> None: + event = "$transaction" mock_response = mock.Mock() - mock_response.content = ('{"status": 0, "error_message": "OK", "workflow_statuses": %s}' - % workflow_statuses_json()) + mock_response.content = f'{{"status": 0, "error_message": "OK", "workflow_statuses": {workflow_statuses_json()}}}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.track( event, valid_transaction_properties(), return_workflow_status=True, return_route_info=True, - abuse_types=['payment_abuse', 'content_abuse', 'legacy']) + abuse_types=["payment_abuse", "content_abuse", "legacy"], + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'return_workflow_status': 'true', 'return_route_info': 'true', - 'abuse_types': 'payment_abuse,content_abuse,legacy'}) + params={ + "return_workflow_status": "true", + "return_route_info": "true", + "abuse_types": "payment_abuse,content_abuse,legacy", + }, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") - assert (response.body['workflow_statuses']['route']['name'] == 'my route') - - def test_get_decisions_fails(self): + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert ( + response.body["workflow_statuses"]["route"]["name"] + == "my route" + ) + + def test_get_decisions_fails(self) -> None: with self.assertRaises(ValueError): - self.sift_client.get_decisions('usr') + self.sift_client.get_decisions( + t.cast(t.Literal["user", "order", "session", "content"], "usr") + ) - def test_get_decisions(self): + def test_get_decisions(self) -> None: mock_response = mock.Mock() get_decisions_response_json = """ @@ -600,31 +650,39 @@ def test_get_decisions(self): "next_ref": "v3/accounts/accountId/decisions" } """ - mock_response.content = get_decisions_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_decisions(entity_type="user", - limit=10, - start_from=None, - abuse_types="legacy,payment_abuse", - timeout=3) + response = self.sift_client.get_decisions( + entity_type="user", + limit=10, + start_from=None, + abuse_types="legacy,payment_abuse", + timeout=3, + ) + mock_get.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/decisions', + "https://api.sift.com/v3/accounts/ACCT/decisions", headers=mock.ANY, auth=mock.ANY, - params={'entity_type': 'user', 'limit': 10, 'abuse_types': 'legacy,payment_abuse'}, - timeout=3) - + params={ + "entity_type": "user", + "limit": 10, + "abuse_types": "legacy,payment_abuse", + }, + timeout=3, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.body['data'][0]['id'] == 'block_user') + assert response.is_ok() + assert isinstance(response.body, dict) + assert response.body["data"][0]["id"] == "block_user" - def test_get_decisions_entity_session(self): + def test_get_decisions_entity_session(self) -> None: mock_response = mock.Mock() get_decisions_response_json = """ { @@ -647,39 +705,47 @@ def test_get_decisions_entity_session(self): "next_ref": "v3/accounts/accountId/decisions" } """ - mock_response.content = get_decisions_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_decisions(entity_type="session", - limit=10, - start_from=None, - abuse_types="account_takeover", - timeout=3) + response = self.sift_client.get_decisions( + entity_type="session", + limit=10, + start_from=None, + abuse_types="account_takeover", + timeout=3, + ) + mock_get.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/decisions', + "https://api.sift.com/v3/accounts/ACCT/decisions", headers=mock.ANY, auth=mock.ANY, - params={'entity_type': 'session', 'limit': 10, 'abuse_types': 'account_takeover'}, - timeout=3) - + params={ + "entity_type": "session", + "limit": 10, + "abuse_types": "account_takeover", + }, + timeout=3, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.body['data'][0]['id'] == 'block_session') + assert response.is_ok() + assert isinstance(response.body, dict) + assert response.body["data"][0]["id"] == "block_session" - def test_apply_decision_to_user_ok(self): - user_id = '54321' + def test_apply_decision_to_user_ok(self) -> None: + user_id = "54321" mock_response = mock.Mock() apply_decision_request = { - 'decision_id': 'user_looks_ok_legacy', - 'source': 'MANUAL_REVIEW', - 'analyst': 'analyst@biz.com', - 'description': 'called user and verified account', - 'time': 1481569575 + "decision_id": "user_looks_ok_legacy", + "source": "MANUAL_REVIEW", + "analyst": "analyst@biz.com", + "description": "called user and verified account", + "time": 1481569575, } apply_decision_response_json = """ { @@ -697,108 +763,143 @@ def test_apply_decision_to_user_ok(self): mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.apply_user_decision(user_id, apply_decision_request) - data = json.dumps(apply_decision_request) - mock_post.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/users/%s/decisions' % user_id, - auth=mock.ANY, data=data, headers=mock.ANY, timeout=mock.ANY) + response = self.sift_client.apply_user_decision( + user_id, apply_decision_request + ) + + mock_post.assert_called_with( + f"https://api.sift.com/v3/accounts/ACCT/users/{user_id}/decisions", + auth=mock.ANY, + data=json.dumps(apply_decision_request), + headers=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.body['entity']['type'] == 'user') - assert (response.http_status_code == 200) - assert (response.is_ok()) + assert response.is_ok() + assert response.http_status_code == 200 + assert isinstance(response.body, dict) + assert response.body["entity"]["type"] == "user" - def test_validate_no_user_id_string_fails(self): + def test_validate_no_user_id_string_fails(self) -> None: apply_decision_request = { - 'decision_id': 'user_looks_ok_legacy', - 'source': 'MANUAL_REVIEW', - 'analyst': 'analyst@biz.com', - 'description': 'called user and verified account', + "decision_id": "user_looks_ok_legacy", + "source": "MANUAL_REVIEW", + "analyst": "analyst@biz.com", + "description": "called user and verified account", } + with self.assertRaises(TypeError): - self.sift_client._validate_apply_decision_request(apply_decision_request, 123) + self.sift_client._validate_apply_decision_request( + apply_decision_request, t.cast(str, 123) + ) - def test_apply_decision_to_order_fails_with_no_order_id(self): + def test_apply_decision_to_order_fails_with_no_order_id(self) -> None: with self.assertRaises(TypeError): - self.sift_client.apply_order_decision("user_id", None, {}) + self.sift_client.apply_order_decision( + "user_id", t.cast(str, None), {} + ) - def test_apply_decision_to_session_fails_with_no_session_id(self): + def test_apply_decision_to_session_fails_with_no_session_id(self) -> None: with self.assertRaises(TypeError): - self.sift_client.apply_session_decision("user_id", None, {}) + self.sift_client.apply_session_decision( + "user_id", t.cast(str, None), {} + ) - def test_get_session_decisions_fails_with_no_session_id(self): + def test_get_session_decisions_fails_with_no_session_id(self) -> None: with self.assertRaises(TypeError): - self.sift_client.get_session_decisions("user_id", None) + self.sift_client.get_session_decisions( + "user_id", t.cast(str, None) + ) - def test_apply_decision_to_content_fails_with_no_content_id(self): + def test_apply_decision_to_content_fails_with_no_content_id(self) -> None: with self.assertRaises(TypeError): - self.sift_client.apply_content_decision("user_id", None, {}) + self.sift_client.apply_content_decision( + "user_id", t.cast(str, None), {} + ) - def test_validate_apply_decision_request_no_analyst_fails(self): + def test_validate_apply_decision_request_no_analyst_fails(self) -> None: apply_decision_request = { - 'decision_id': 'user_looks_ok_legacy', - 'source': 'MANUAL_REVIEW', - 'time': 1481569575 + "decision_id": "user_looks_ok_legacy", + "source": "MANUAL_REVIEW", + "time": 1481569575, } with self.assertRaises(ValueError): - self.sift_client._validate_apply_decision_request(apply_decision_request, "userId") + self.sift_client._validate_apply_decision_request( + apply_decision_request, "userId" + ) - def test_validate_apply_decision_request_no_source_fails(self): + def test_validate_apply_decision_request_no_source_fails(self) -> None: apply_decision_request = { - 'decision_id': 'user_looks_ok_legacy', - 'time': 1481569575 + "decision_id": "user_looks_ok_legacy", + "time": 1481569575, } with self.assertRaises(ValueError): - self.sift_client._validate_apply_decision_request(apply_decision_request, "userId") + self.sift_client._validate_apply_decision_request( + apply_decision_request, "userId" + ) + + def test_validate_empty_apply_decision_request_fails(self) -> None: + apply_decision_request: dict[str, t.Any] = {} - def test_validate_empty_apply_decision_request_fails(self): - apply_decision_request = {} with self.assertRaises(ValueError): - self.sift_client._validate_apply_decision_request(apply_decision_request, "userId") + self.sift_client._validate_apply_decision_request( + apply_decision_request, "userId" + ) - def test_apply_decision_manual_review_no_analyst_fails(self): - user_id = '54321' + def test_apply_decision_manual_review_no_analyst_fails(self) -> None: + user_id = "54321" apply_decision_request = { - 'decision_id': 'user_looks_ok_legacy', - 'source': 'MANUAL_REVIEW', - 'time': 1481569575 + "decision_id": "user_looks_ok_legacy", + "source": "MANUAL_REVIEW", + "time": 1481569575, } with self.assertRaises(ValueError): - self.sift_client.apply_user_decision(user_id, apply_decision_request) + self.sift_client.apply_user_decision( + user_id, apply_decision_request + ) - def test_apply_decision_no_source_fails(self): - user_id = '54321' + def test_apply_decision_no_source_fails(self) -> None: + user_id = "54321" apply_decision_request = { - 'decision_id': 'user_looks_ok_legacy', - 'time': 1481569575 + "decision_id": "user_looks_ok_legacy", + "time": 1481569575, } with self.assertRaises(ValueError): - self.sift_client.apply_user_decision(user_id, apply_decision_request) + self.sift_client.apply_user_decision( + user_id, apply_decision_request + ) - def test_apply_decision_invalid_source_fails(self): - user_id = '54321' + def test_apply_decision_invalid_source_fails(self) -> None: + user_id = "54321" apply_decision_request = { - 'decision_id': 'user_looks_ok_legacy', - 'source': 'INVALID_SOURCE', - 'time': 1481569575 + "decision_id": "user_looks_ok_legacy", + "source": "INVALID_SOURCE", + "time": 1481569575, } - self.assertRaises(ValueError, self.sift_client.apply_user_decision, user_id, apply_decision_request) + self.assertRaises( + ValueError, + self.sift_client.apply_user_decision, + user_id, + apply_decision_request, + ) - def test_apply_decision_to_order_ok(self): - user_id = '54321' - order_id = '43210' + def test_apply_decision_to_order_ok(self) -> None: + user_id = "54321" + order_id = "43210" mock_response = mock.Mock() apply_decision_request = { - 'decision_id': 'order_looks_bad_payment_abuse', - 'source': 'AUTOMATED_RULE', - 'time': 1481569575 + "decision_id": "order_looks_bad_payment_abuse", + "source": "AUTOMATED_RULE", + "time": 1481569575, } apply_decision_response_json = """ @@ -813,31 +914,39 @@ def test_apply_decision_to_order_ok(self): "time": "1481569575" } """ - mock_response.content = apply_decision_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.apply_order_decision(user_id, order_id, apply_decision_request) - data = json.dumps(apply_decision_request) + + response = self.sift_client.apply_order_decision( + user_id, order_id, apply_decision_request + ) + mock_post.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/users/%s/orders/%s/decisions' % (user_id, order_id), - auth=mock.ANY, data=data, headers=mock.ANY, timeout=mock.ANY) + f"https://api.sift.com/v3/accounts/ACCT/users/{user_id}/orders/{order_id}/decisions", + auth=mock.ANY, + data=json.dumps(apply_decision_request), + headers=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.http_status_code == 200) - assert (response.body['entity']['type'] == 'order') - - def test_apply_decision_to_session_ok(self): - user_id = '54321' - session_id = 'gigtleqddo84l8cm15qe4il' + assert response.is_ok() + assert response.http_status_code == 200 + assert isinstance(response.body, dict) + assert response.body["entity"]["type"] == "order" + + def test_apply_decision_to_session_ok(self) -> None: + user_id = "54321" + session_id = "gigtleqddo84l8cm15qe4il" mock_response = mock.Mock() apply_decision_request = { - 'decision_id': 'session_looks_bad_ato', - 'source': 'AUTOMATED_RULE', - 'time': 1481569575 + "decision_id": "session_looks_bad_ato", + "source": "AUTOMATED_RULE", + "time": 1481569575, } apply_decision_response_json = """ @@ -852,31 +961,39 @@ def test_apply_decision_to_session_ok(self): "time": "1481569575" } """ - mock_response.content = apply_decision_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.apply_session_decision(user_id, session_id, apply_decision_request) - data = json.dumps(apply_decision_request) + + response = self.sift_client.apply_session_decision( + user_id, session_id, apply_decision_request + ) + mock_post.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/users/%s/sessions/%s/decisions' % (user_id, session_id), - auth=mock.ANY, data=data, headers=mock.ANY, timeout=mock.ANY) + f"https://api.sift.com/v3/accounts/ACCT/users/{user_id}/sessions/{session_id}/decisions", + auth=mock.ANY, + data=json.dumps(apply_decision_request), + headers=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.http_status_code == 200) - assert (response.body['entity']['type'] == 'login') - - def test_apply_decision_to_content_ok(self): - user_id = '54321' - content_id = 'listing-1231' + assert response.is_ok() + assert response.http_status_code == 200 + assert isinstance(response.body, dict) + assert response.body["entity"]["type"] == "login" + + def test_apply_decision_to_content_ok(self) -> None: + user_id = "54321" + content_id = "listing-1231" mock_response = mock.Mock() apply_decision_request = { - 'decision_id': 'content_looks_bad_content_abuse', - 'source': 'AUTOMATED_RULE', - 'time': 1481569575 + "decision_id": "content_looks_bad_content_abuse", + "source": "AUTOMATED_RULE", + "time": 1481569575, } apply_decision_response_json = """ @@ -891,247 +1008,290 @@ def test_apply_decision_to_content_ok(self): "time": "1481569575" } """ - mock_response.content = apply_decision_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.apply_content_decision(user_id, content_id, apply_decision_request) - data = json.dumps(apply_decision_request) + + response = self.sift_client.apply_content_decision( + user_id, content_id, apply_decision_request + ) + mock_post.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/users/%s/content/%s/decisions' % (user_id, content_id), - auth=mock.ANY, data=data, headers=mock.ANY, timeout=mock.ANY) + f"https://api.sift.com/v3/accounts/ACCT/users/{user_id}/content/{content_id}/decisions", + auth=mock.ANY, + data=json.dumps(apply_decision_request), + headers=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.http_status_code == 200) - assert (response.body['entity']['type'] == 'create_content') + assert response.is_ok() + assert response.http_status_code == 200 + assert isinstance(response.body, dict) + assert response.body["entity"]["type"] == "create_content" - def test_label_user_ok(self): - user_id = '54321' + def test_label_user_ok(self) -> None: + user_id = "54321" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.label(user_id, valid_label_properties()) + + response = self.sift_client.label( + user_id, valid_label_properties() + ) + properties = { - '$abuse_type': 'content_abuse', - '$is_bad': True, - '$description': 'Listed a fake item', - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$abuse_type": "content_abuse", + "$is_bad": True, + "$description": "Listed a fake item", + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", + "$api_key": self.test_key, + "$type": "$label", } - properties.update({'$api_key': self.test_key, '$type': '$label'}) - data = json.dumps(properties) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/users/%s/labels' % user_id, - data=data, headers=mock.ANY, timeout=mock.ANY, params={}) + f"https://api.sift.com/v205/users/{user_id}/labels", + data=json.dumps(properties), + headers=mock.ANY, + timeout=mock.ANY, + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_label_user_with_timeout_param_ok(self): - user_id = '54321' + def test_label_user_with_timeout_param_ok(self) -> None: + user_id = "54321" test_timeout = 5 mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.label( - user_id, valid_label_properties(), test_timeout) + user_id, valid_label_properties(), test_timeout + ) + properties = { - '$abuse_type': 'content_abuse', - '$is_bad': True, - '$description': 'Listed a fake item', - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$abuse_type": "content_abuse", + "$is_bad": True, + "$description": "Listed a fake item", + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", + "$api_key": self.test_key, + "$type": "$label", } - properties.update({'$api_key': self.test_key, '$type': '$label'}) - data = json.dumps(properties) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/users/%s/labels' % user_id, - data=data, headers=mock.ANY, timeout=test_timeout, params={}) + f"https://api.sift.com/v205/users/{user_id}/labels", + data=json.dumps(properties), + headers=mock.ANY, + timeout=test_timeout, + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_unlabel_user_ok(self): - user_id = '54321' + def test_unlabel_user_ok(self) -> None: + user_id = "54321" mock_response = mock.Mock() mock_response.status_code = 204 - with mock.patch.object(self.sift_client.session, 'delete') as mock_delete: + + with mock.patch.object( + self.sift_client.session, "delete" + ) as mock_delete: mock_delete.return_value = mock_response - response = self.sift_client.unlabel(user_id, abuse_type='account_abuse') + + response = self.sift_client.unlabel( + user_id, abuse_type="account_abuse" + ) + mock_delete.assert_called_with( - 'https://api.siftscience.com/v205/users/%s/labels' % user_id, + f"https://api.sift.com/v205/users/{user_id}/labels", headers=mock.ANY, timeout=mock.ANY, - params={'abuse_type': 'account_abuse'}, - auth=HTTPBasicAuth(self.test_key, '')) + params={"abuse_type": "account_abuse"}, + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - - def test_unicode_string_parameter_support(self): - # str is unicode in python 3, so no need to check as this was covered - # by other unit tests. - if sys.version_info[0] < 3: - mock_response = mock.Mock() - mock_response.content = '{"status": 0, "error_message": "OK"}' - mock_response.json.return_value = json.loads(mock_response.content) - mock_response.status_code = 200 - mock_response.headers = response_with_data_header() - - user_id = '23056' - - with mock.patch.object(self.sift_client.session, 'post') as mock_post: - mock_post.return_value = mock_response - assert (self.sift_client.track( - '$transaction', - valid_transaction_properties())) - assert (self.sift_client.label( - user_id, - valid_label_properties())) - with mock.patch.object(self.sift_client.session, 'get') as mock_get: - mock_get.return_value = mock_response - assert (self.sift_client.score( - user_id, abuse_types=['payment_abuse', 'content_abuse'])) - - def test_unlabel_user_with_special_chars_ok(self): + assert response.is_ok() + + def test_unlabel_user_with_special_chars_ok(self) -> None: user_id = "54321=.-_+@:&^%!$" mock_response = mock.Mock() mock_response.status_code = 204 - with mock.patch.object(self.sift_client.session, 'delete') as mock_delete: + + with mock.patch.object( + self.sift_client.session, "delete" + ) as mock_delete: mock_delete.return_value = mock_response + response = self.sift_client.unlabel(user_id) + mock_delete.assert_called_with( - 'https://api.siftscience.com/v205/users/%s/labels' % urllib.parse.quote(user_id), + f"https://api.sift.com/v205/users/{_q(user_id)}/labels", headers=mock.ANY, timeout=mock.ANY, params={}, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) + assert response.is_ok() - def test_label_user__with_special_chars_ok(self): - user_id = '54321=.-_+@:&^%!$' + def test_label_user__with_special_chars_ok(self) -> None: + user_id = "54321=.-_+@:&^%!$" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.label( - user_id, valid_label_properties()) + user_id, valid_label_properties() + ) + properties = { - '$abuse_type': 'content_abuse', - '$is_bad': True, - '$description': 'Listed a fake item', - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$abuse_type": "content_abuse", + "$is_bad": True, + "$description": "Listed a fake item", + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", + "$api_key": self.test_key, + "$type": "$label", } - properties.update({'$api_key': self.test_key, '$type': '$label'}) - data = json.dumps(properties) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/users/%s/labels' % urllib.parse.quote(user_id), - data=data, + f"https://api.sift.com/v205/users/{_q(user_id)}/labels", + data=json.dumps(properties), headers=mock.ANY, timeout=mock.ANY, - params={}) + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_score__with_special_user_id_chars_ok(self): - user_id = '54321=.-_+@:&^%!$' + def test_score__with_special_user_id_chars_ok(self) -> None: + user_id = "54321=.-_+@:&^%!$" mock_response = mock.Mock() mock_response.content = score_response_json() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.score(user_id, abuse_types=['legacy']) + + response = self.sift_client.score(user_id, abuse_types=["legacy"]) + mock_get.assert_called_with( - 'https://api.siftscience.com/v205/score/%s' % urllib.parse.quote(user_id), - params={'abuse_types': 'legacy'}, + f"https://api.sift.com/v205/score/{_q(user_id)}", + params={"abuse_types": "legacy"}, headers=mock.ANY, timeout=mock.ANY, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['score'] == 0.85) - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - - def test_exception_during_track_call(self): + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score"] == 0.85 + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + + def test_exception_during_track_call(self) -> None: warnings.simplefilter("always") - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.side_effect = mock.Mock( - side_effect=requests.exceptions.RequestException("Failed")) + side_effect=RequestException("Failed") + ) + with self.assertRaises(sift.client.ApiException): - self.sift_client.track('$transaction', valid_transaction_properties()) + self.sift_client.track( + "$transaction", valid_transaction_properties() + ) - def test_exception_during_score_call(self): + def test_exception_during_score_call(self) -> None: warnings.simplefilter("always") - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.side_effect = mock.Mock( - side_effect=requests.exceptions.RequestException("Failed")) + side_effect=RequestException("Failed") + ) + with self.assertRaises(sift.client.ApiException): - self.sift_client.score('Fred') + self.sift_client.score("Fred") - def test_exception_during_unlabel_call(self): + def test_exception_during_unlabel_call(self) -> None: warnings.simplefilter("always") - with mock.patch.object(self.sift_client.session, 'delete') as mock_delete: + + with mock.patch.object( + self.sift_client.session, "delete" + ) as mock_delete: mock_delete.side_effect = mock.Mock( - side_effect=requests.exceptions.RequestException("Failed")) + side_effect=RequestException("Failed") + ) + with self.assertRaises(sift.client.ApiException): - self.sift_client.unlabel('Fred') + self.sift_client.unlabel("Fred") - def test_return_actions_on_track(self): - event = '$transaction' + def test_return_actions_on_track(self) -> None: + event = "$transaction" mock_response = mock.Mock() - mock_response.content = ('{"status": 0, "error_message": "OK", "score_response": %s}' - % action_response_json()) + mock_response.content = f'{{"status": 0, "error_message": "OK", "score_response": {action_response_json()}}}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response response = self.sift_client.track( - event, valid_transaction_properties(), return_action=True) + event, valid_transaction_properties(), return_action=True + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'return_action': 'true'}) + params={"return_action": "true"}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") - - actions = response.body["score_response"]['actions'] - assert (actions) - assert (actions[0]['action']) - assert (actions[0]['action']['id'] == 'freds_action') - assert (actions[0]['triggers']) - - def test_get_workflow_status(self): + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + + actions = response.body["score_response"]["actions"] + assert actions + assert actions[0]["action"] + assert actions[0]["action"]["id"] == "freds_action" + assert actions[0]["triggers"] + + def test_get_workflow_status(self) -> None: mock_response = mock.Mock() mock_response.content = """ { @@ -1177,19 +1337,25 @@ def test_get_workflow_status(self): mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_workflow_status('4zxwibludiaaa', timeout=3) - mock_get.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/workflows/runs/4zxwibludiaaa', - headers=mock.ANY, auth=mock.ANY, timeout=3) + response = self.sift_client.get_workflow_status( + "4zxwibludiaaa", timeout=3 + ) + mock_get.assert_called_with( + "https://api.sift.com/v3/accounts/ACCT/workflows/runs/4zxwibludiaaa", + headers=mock.ANY, + auth=mock.ANY, + timeout=3, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.body['state'] == 'running') + assert response.is_ok() + assert isinstance(response.body, dict) + assert response.body["state"] == "running" - def test_get_user_decisions(self): + def test_get_user_decisions(self) -> None: mock_response = mock.Mock() mock_response.content = """ { @@ -1208,19 +1374,26 @@ def test_get_user_decisions(self): mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_user_decisions('example_user') - mock_get.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/users/example_user/decisions', - headers=mock.ANY, auth=mock.ANY, timeout=mock.ANY) + response = self.sift_client.get_user_decisions("example_user") + mock_get.assert_called_with( + "https://api.sift.com/v3/accounts/ACCT/users/example_user/decisions", + headers=mock.ANY, + auth=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.body['decisions']['payment_abuse']['decision']['id'] == 'user_decision') - - def test_get_order_decisions(self): + assert response.is_ok() + assert isinstance(response.body, dict) + assert ( + response.body["decisions"]["payment_abuse"]["decision"]["id"] + == "user_decision" + ) + + def test_get_order_decisions(self) -> None: mock_response = mock.Mock() mock_response.content = """ { @@ -1246,20 +1419,30 @@ def test_get_order_decisions(self): mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_order_decisions('example_order') - mock_get.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/orders/example_order/decisions', - headers=mock.ANY, auth=mock.ANY, timeout=mock.ANY) + response = self.sift_client.get_order_decisions("example_order") + mock_get.assert_called_with( + "https://api.sift.com/v3/accounts/ACCT/orders/example_order/decisions", + headers=mock.ANY, + auth=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.body['decisions']['payment_abuse']['decision']['id'] == 'decision7') - assert (response.body['decisions']['promotion_abuse']['decision']['id'] == 'good_order') - - def test_get_session_decisions(self): + assert response.is_ok() + assert isinstance(response.body, dict) + assert ( + response.body["decisions"]["payment_abuse"]["decision"]["id"] + == "decision7" + ) + assert ( + response.body["decisions"]["promotion_abuse"]["decision"]["id"] + == "good_order" + ) + + def test_get_session_decisions(self) -> None: mock_response = mock.Mock() mock_response.content = """ { @@ -1278,19 +1461,30 @@ def test_get_session_decisions(self): mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_session_decisions('example_user', 'example_session') - mock_get.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/users/example_user/sessions/example_session/decisions', - headers=mock.ANY, auth=mock.ANY, timeout=mock.ANY) + response = self.sift_client.get_session_decisions( + "example_user", "example_session" + ) + mock_get.assert_called_with( + "https://api.sift.com/v3/accounts/ACCT/users/example_user/sessions/example_session/decisions", + headers=mock.ANY, + auth=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.body['decisions']['account_takeover']['decision']['id'] == 'session_decision') + assert response.is_ok() + assert isinstance(response.body, dict) + assert ( + response.body["decisions"]["account_takeover"]["decision"][ + "id" + ] + == "session_decision" + ) - def test_get_content_decisions(self): + def test_get_content_decisions(self) -> None: mock_response = mock.Mock() mock_response.content = """ { @@ -1309,21 +1503,34 @@ def test_get_content_decisions(self): mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_content_decisions('example_user', 'example_content') - mock_get.assert_called_with( - 'https://api3.siftscience.com/v3/accounts/ACCT/users/example_user/content/example_content/decisions', - headers=mock.ANY, auth=mock.ANY, timeout=mock.ANY) + response = self.sift_client.get_content_decisions( + "example_user", "example_content" + ) + mock_get.assert_called_with( + "https://api.sift.com/v3/accounts/ACCT/users/example_user/content/example_content/decisions", + headers=mock.ANY, + auth=mock.ANY, + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.body['decisions']['content_abuse']['decision']['id'] == 'content_looks_bad_content_abuse') - - def test_provided_session(self): + assert response.is_ok() + assert isinstance(response.body, dict) + assert ( + response.body["decisions"]["content_abuse"]["decision"]["id"] + == "content_looks_bad_content_abuse" + ) + + def test_provided_session(self) -> None: session = mock.Mock() - client = sift.Client(api_key=self.test_key, account_id=self.account_id, session=session) + client = sift.Client( + api_key=self.test_key, + account_id=self.account_id, + session=session, + ) mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' @@ -1331,12 +1538,13 @@ def test_provided_session(self): mock_response.status_code = 200 mock_response.headers = response_with_data_header() session.post.return_value = mock_response + event = "$transaction" - event = '$transaction' client.track(event, valid_transaction_properties()) + session.post.assert_called_once() - def test_get_psp_merchant_profile(self): + def test_get_psp_merchant_profile(self) -> None: """Test the GET /{version}/accounts/{accountId}/scorepsp_management/merchants?batch_type=...""" test_timeout = 5 mock_response = mock.Mock() @@ -1344,149 +1552,192 @@ def test_get_psp_merchant_profile(self): mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_post: + + with mock.patch.object(self.sift_client.session, "get") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.get_psp_merchant_profiles( - timeout=test_timeout) + timeout=test_timeout + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v3/accounts/ACCT/psp_management/merchants', + "https://api.sift.com/v3/accounts/ACCT/psp_management/merchants", params={}, - headers=mock.ANY, auth=mock.ANY, - timeout=test_timeout) + headers=mock.ANY, + auth=mock.ANY, + timeout=test_timeout, + ) self.assertIsInstance(response, sift.client.Response) - assert ('address' in response.body) + assert isinstance(response.body, dict) + assert "address" in response.body - def test_get_psp_merchant_profile_id(self): - """Test the GET /{version}/accounts/{accountId}/scorepsp_management/merchants/{merchantId} - """ + def test_get_psp_merchant_profile_id(self) -> None: + """Test the GET /{version}/accounts/{accountId}/scorepsp_management/merchants/{merchantId}""" test_timeout = 5 mock_response = mock.Mock() mock_response.content = valid_psp_merchant_properties_response() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_post: + + with mock.patch.object(self.sift_client.session, "get") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.get_a_psp_merchant_profile( - merchant_id='api-key-1', timeout=test_timeout) + merchant_id="api-key-1", timeout=test_timeout + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v3/accounts/ACCT/psp_management/merchants/api-key-1', + "https://api.sift.com/v3/accounts/ACCT/psp_management/merchants/api-key-1", headers=mock.ANY, auth=mock.ANY, - timeout=test_timeout) + timeout=test_timeout, + ) self.assertIsInstance(response, sift.client.Response) - assert ('address' in response.body) + assert isinstance(response.body, dict) + assert "address" in response.body - def test_create_psp_merchant_profile(self): + def test_create_psp_merchant_profile(self) -> None: mock_response = mock.Mock() mock_response.content = valid_psp_merchant_properties_response() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response response = self.sift_client.create_psp_merchant_profile( - valid_psp_merchant_properties()) + valid_psp_merchant_properties() + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v3/accounts/ACCT/psp_management/merchants', + "https://api.sift.com/v3/accounts/ACCT/psp_management/merchants", data=json.dumps(valid_psp_merchant_properties()), headers=mock.ANY, auth=mock.ANY, - timeout=mock.ANY) - + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert ('address' in response.body) + assert isinstance(response.body, dict) + assert "address" in response.body - def test_update_psp_merchant_profile(self): + def test_update_psp_merchant_profile(self) -> None: mock_response = mock.Mock() mock_response.content = valid_psp_merchant_properties_response() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'put') as mock_post: + with mock.patch.object(self.sift_client.session, "put") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.update_psp_merchant_profile('api-key-1', - valid_psp_merchant_properties()) + response = self.sift_client.update_psp_merchant_profile( + "api-key-1", valid_psp_merchant_properties() + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v3/accounts/ACCT/psp_management/merchants/api-key-1', + "https://api.sift.com/v3/accounts/ACCT/psp_management/merchants/api-key-1", data=json.dumps(valid_psp_merchant_properties()), headers=mock.ANY, auth=mock.ANY, - timeout=mock.ANY) - + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert ('address' in response.body) + assert isinstance(response.body, dict) + assert "address" in response.body - def test_with_include_score_percentiles_ok(self): - event = '$transaction' + def test_with_include_score_percentiles_ok(self) -> None: + event = "$transaction" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.track(event, valid_transaction_properties(), include_score_percentiles=True) + + response = self.sift_client.track( + event, + valid_transaction_properties(), + include_score_percentiles=True, + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'fields': 'SCORE_PERCENTILES'}) + params={"fields": "SCORE_PERCENTILES"}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_include_score_percentiles_as_false_ok(self): - event = '$transaction' + def test_include_score_percentiles_as_false_ok(self) -> None: + event = "$transaction" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.track(event, valid_transaction_properties(), include_score_percentiles=False) + + response = self.sift_client.track( + event, + valid_transaction_properties(), + include_score_percentiles=False, + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={}) + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_status == 0) - assert (response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_score_api_include_score_percentiles_ok(self): + def test_score_api_include_score_percentiles_ok(self) -> None: mock_response = mock.Mock() mock_response.content = score_response_json() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.score(user_id='12345', include_score_percentiles=True) + + response = self.sift_client.score( + user_id="12345", include_score_percentiles=True + ) + mock_get.assert_called_with( - 'https://api.siftscience.com/v205/score/12345', - params={'fields': 'SCORE_PERCENTILES'}, + "https://api.sift.com/v205/score/12345", + params={"fields": "SCORE_PERCENTILES"}, headers=mock.ANY, timeout=mock.ANY, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['score'] == 0.85) - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - - def test_get_user_score_include_score_percentiles_ok(self): - """Test the GET /{version}/users/{userId}/score API, i.e. client.get_user_score() + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score"] == 0.85 + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + + def test_get_user_score_include_score_percentiles_ok(self) -> None: + """ + Test the GET /{version}/users/{userId}/score API, + i.e. client.get_user_score() """ test_timeout = 5 mock_response = mock.Mock() @@ -1494,64 +1745,85 @@ def test_get_user_score_include_score_percentiles_ok(self): mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.get_user_score(user_id='12345', timeout=test_timeout, include_score_percentiles=True) + + response = self.sift_client.get_user_score( + user_id="12345", + timeout=test_timeout, + include_score_percentiles=True, + ) + mock_get.assert_called_with( - 'https://api.siftscience.com/v205/users/12345/score', - params={'fields': 'SCORE_PERCENTILES'}, + "https://api.sift.com/v205/users/12345/score", + params={"fields": "SCORE_PERCENTILES"}, headers=mock.ANY, timeout=test_timeout, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert (response.is_ok()) - assert (response.api_error_message == "OK") - assert (response.body['entity_id'] == '12345') - assert (response.body['scores']['content_abuse']['score'] == 0.14) - assert (response.body['scores']['payment_abuse']['score'] == 0.97) - assert ('latest_decisions' in response.body) - - def test_warnings_added_as_fields_param(self): - event = '$transaction' + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["entity_id"] == "12345" + assert response.body["scores"]["content_abuse"]["score"] == 0.14 + assert response.body["scores"]["payment_abuse"]["score"] == 0.97 + assert "latest_decisions" in response.body + + def test_warnings_added_as_fields_param(self) -> None: + event = "$transaction" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.track(event, valid_transaction_properties(), - include_warnings=True) + + response = self.sift_client.track( + event, valid_transaction_properties(), include_warnings=True + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'fields': 'WARNINGS'}) + params={"fields": "WARNINGS"}, + ) self.assertIsInstance(response, sift.client.Response) - def test_warnings_and_score_percentiles_added_as_fields_param(self): - event = '$transaction' + def test_warnings_and_score_percentiles_added_as_fields_param( + self, + ) -> None: + event = "$transaction" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.track(event, valid_transaction_properties(), - include_score_percentiles=True, - include_warnings=True) + response = self.sift_client.track( + event, + valid_transaction_properties(), + include_score_percentiles=True, + include_warnings=True, + ) mock_post.assert_called_with( - 'https://api.siftscience.com/v205/events', + "https://api.sift.com/v205/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'fields': 'SCORE_PERCENTILES,WARNINGS'}) + params={"fields": "SCORE_PERCENTILES,WARNINGS"}, + ) self.assertIsInstance(response, sift.client.Response) -def main(): - unittest.main() +def main() -> None: + main() -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/tests/test_client_v203.py b/tests/test_client_v203.py index 606d741..24a4f3c 100644 --- a/tests/test_client_v203.py +++ b/tests/test_client_v203.py @@ -1,50 +1,51 @@ +from __future__ import annotations + import datetime -from decimal import Decimal -import warnings import json -import mock -import sift +import typing as t import unittest -import sys -import requests.exceptions +import warnings +from decimal import Decimal +from unittest import mock + from requests.auth import HTTPBasicAuth -if sys.version_info[0] < 3: - import six.moves.urllib as urllib -else: - import urllib.parse +from requests.exceptions import RequestException + +import sift +from sift.utils import quote_path as _q -def valid_transaction_properties(): +def valid_transaction_properties() -> dict[str, t.Any]: return { - '$buyer_user_id': '123456', - '$seller_user_id': '654321', - '$amount': Decimal('1253200.0'), - '$currency_code': 'USD', - '$time': int(datetime.datetime.now().strftime('%S')), - '$transaction_id': 'my_transaction_id', - '$billing_name': 'Mike Snow', - '$billing_bin': '411111', - '$billing_last4': '1111', - '$billing_address1': '123 Main St.', - '$billing_city': 'San Francisco', - '$billing_region': 'CA', - '$billing_country': 'US', - '$billing_zip': '94131', - '$user_email': 'mike@example.com' + "$buyer_user_id": "123456", + "$seller_user_id": "654321", + "$amount": Decimal("1253200.0"), + "$currency_code": "USD", + "$time": int(datetime.datetime.now().strftime("%S")), + "$transaction_id": "my_transaction_id", + "$billing_name": "Mike Snow", + "$billing_bin": "411111", + "$billing_last4": "1111", + "$billing_address1": "123 Main St.", + "$billing_city": "San Francisco", + "$billing_region": "CA", + "$billing_country": "US", + "$billing_zip": "94131", + "$user_email": "mike@example.com", } -def valid_label_properties(): +def valid_label_properties() -> dict[str, t.Any]: return { - '$description': 'Listed a fake item', - '$is_bad': True, - '$reasons': ["$fake"], - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$description": "Listed a fake item", + "$is_bad": True, + "$reasons": ["$fake"], + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", } -def score_response_json(): +def score_response_json() -> str: return """{ "status": 0, "error_message": "OK", @@ -53,7 +54,7 @@ def score_response_json(): }""" -def action_response_json(): +def action_response_json() -> str: return """{ "actions": [ { @@ -82,371 +83,447 @@ def action_response_json(): }""" -def response_with_data_header(): +def response_with_data_header() -> dict[str, t.Any]: return { - 'content-length': 1, # Simply has to be > 0 - 'content-type': 'application/json; charset=UTF-8' + "content-length": 1, # Simply has to be > 0 + "content-type": "application/json; charset=UTF-8", } class TestSiftPythonClient(unittest.TestCase): - def setUp(self): - self.test_key = 'a_fake_test_api_key' - self.sift_client = sift.Client(self.test_key, version='203') - self.sift_client_v204 = sift.Client(self.test_key) + def setUp(self) -> None: + self.test_key = "a_fake_test_api_key" + self.sift_client = sift.Client(api_key=self.test_key, version="203") + self.sift_client_v204 = sift.Client(api_key=self.test_key) - def test_track_requires_valid_event(self): + def test_track_requires_valid_event(self) -> None: self.assertRaises(TypeError, self.sift_client.track, None, {}) - self.assertRaises(ValueError, self.sift_client.track, '', {}) - self.assertRaises(TypeError, self.sift_client_v204.track, 42, {'version': '203'}) + self.assertRaises(ValueError, self.sift_client.track, "", {}) + self.assertRaises( + TypeError, self.sift_client_v204.track, 42, {"version": "203"} + ) - def test_track_requires_properties(self): - event = 'custom_event' + def test_track_requires_properties(self) -> None: + event = "custom_event" self.assertRaises(TypeError, self.sift_client.track, event, None, {}) - self.assertRaises(TypeError, self.sift_client_v204.track, event, 42, {'version': '203'}) + self.assertRaises( + TypeError, + self.sift_client_v204.track, + event, + 42, + {"version": "203"}, + ) self.assertRaises(ValueError, self.sift_client.track, event, {}) - def test_score_requires_user_id(self): - self.assertRaises(TypeError, self.sift_client_v204.score, None, {'version': '203'}) - self.assertRaises(ValueError, self.sift_client.score, '', {}) + def test_score_requires_user_id(self) -> None: + self.assertRaises( + TypeError, self.sift_client_v204.score, None, {"version": "203"} + ) + self.assertRaises(ValueError, self.sift_client.score, "", {}) self.assertRaises(TypeError, self.sift_client.score, 42, {}) - def test_event_ok(self): - event = '$transaction' + def test_event_ok(self) -> None: + event = "$transaction" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.track(event, valid_transaction_properties()) + + response = self.sift_client.track( + event, valid_transaction_properties() + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v203/events', + "https://api.sift.com/v203/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={}) + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_event_with_timeout_param_ok(self): - event = '$transaction' + def test_event_with_timeout_param_ok(self) -> None: + event = "$transaction" test_timeout = 5 mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client_v204.session, 'post') as mock_post: + + with mock.patch.object( + self.sift_client_v204.session, "post" + ) as mock_post: mock_post.return_value = mock_response + response = self.sift_client_v204.track( - event, valid_transaction_properties(), timeout=test_timeout, version='203') + event, + valid_transaction_properties(), + timeout=test_timeout, + version="203", + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v203/events', + "https://api.sift.com/v203/events", data=mock.ANY, headers=mock.ANY, timeout=test_timeout, - params={}) + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_score_ok(self): + def test_score_ok(self) -> None: mock_response = mock.Mock() mock_response.content = score_response_json() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client_v204.session, 'get') as mock_get: + + with mock.patch.object( + self.sift_client_v204.session, "get" + ) as mock_get: mock_get.return_value = mock_response - response = self.sift_client_v204.score('12345', version='203') + + response = self.sift_client_v204.score("12345", version="203") + mock_get.assert_called_with( - 'https://api.siftscience.com/v203/score/12345', + "https://api.sift.com/v203/score/12345", params={}, headers=mock.ANY, timeout=mock.ANY, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_error_message == "OK") - assert(response.body['score'] == 0.55) + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score"] == 0.55 - def test_score_with_timeout_param_ok(self): + def test_score_with_timeout_param_ok(self) -> None: test_timeout = 5 mock_response = mock.Mock() mock_response.content = score_response_json() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response - response = self.sift_client.score('12345', test_timeout) + + response = self.sift_client.score("12345", test_timeout) + mock_get.assert_called_with( - 'https://api.siftscience.com/v203/score/12345', + "https://api.sift.com/v203/score/12345", params={}, headers=mock.ANY, timeout=test_timeout, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_error_message == "OK") - assert(response.body['score'] == 0.55) + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score"] == 0.55 - def test_sync_score_ok(self): - event = '$transaction' + def test_sync_score_ok(self) -> None: + event = "$transaction" mock_response = mock.Mock() - mock_response.content = ('{"status": 0, "error_message": "OK", "score_response": %s}' - % score_response_json()) + mock_response.content = f'{{"status": 0, "error_message": "OK", "score_response": {score_response_json()}}}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.track( - event, valid_transaction_properties(), return_score=True) + event, valid_transaction_properties(), return_score=True + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v203/events', + "https://api.sift.com/v203/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'return_score': 'true'}) + params={"return_score": "true"}, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") - assert(response.body["score_response"]['score'] == 0.55) - - def test_label_user_ok(self): - user_id = '54321' + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score_response"]["score"] == 0.55 + + def test_label_user_ok(self) -> None: + user_id = "54321" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.label(user_id, valid_label_properties()) + + response = self.sift_client.label( + user_id, valid_label_properties() + ) + properties = { - '$description': 'Listed a fake item', - '$is_bad': True, - '$reasons': ["$fake"], - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$description": "Listed a fake item", + "$is_bad": True, + "$reasons": ["$fake"], + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", } - properties.update({'$api_key': self.test_key, '$type': '$label'}) + properties.update({"$api_key": self.test_key, "$type": "$label"}) data = json.dumps(properties) mock_post.assert_called_with( - 'https://api.siftscience.com/v203/users/%s/labels' % user_id, - data=data, headers=mock.ANY, timeout=mock.ANY, params={}) + f"https://api.sift.com/v203/users/{user_id}/labels", + data=data, + headers=mock.ANY, + timeout=mock.ANY, + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_label_user_with_timeout_param_ok(self): - user_id = '54321' + def test_label_user_with_timeout_param_ok(self) -> None: + user_id = "54321" test_timeout = 5 mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client_v204.session, 'post') as mock_post: + + with mock.patch.object( + self.sift_client_v204.session, "post" + ) as mock_post: mock_post.return_value = mock_response + response = self.sift_client_v204.label( - user_id, valid_label_properties(), test_timeout, version='203') + user_id, valid_label_properties(), test_timeout, version="203" + ) + properties = { - '$description': 'Listed a fake item', - '$is_bad': True, - '$reasons': ["$fake"], - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$description": "Listed a fake item", + "$is_bad": True, + "$reasons": ["$fake"], + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", + "$api_key": self.test_key, + "$type": "$label", } - properties.update({'$api_key': self.test_key, '$type': '$label'}) - data = json.dumps(properties) + mock_post.assert_called_with( - 'https://api.siftscience.com/v203/users/%s/labels' % user_id, - data=data, headers=mock.ANY, timeout=test_timeout, params={}) + f"https://api.sift.com/v203/users/{user_id}/labels", + data=json.dumps(properties), + headers=mock.ANY, + timeout=test_timeout, + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_unlabel_user_ok(self): - user_id = '54321' + def test_unlabel_user_ok(self) -> None: + user_id = "54321" mock_response = mock.Mock() mock_response.status_code = 204 - with mock.patch.object(self.sift_client.session, 'delete') as mock_delete: + + with mock.patch.object( + self.sift_client.session, "delete" + ) as mock_delete: mock_delete.return_value = mock_response + response = self.sift_client.unlabel(user_id) + mock_delete.assert_called_with( - 'https://api.siftscience.com/v203/users/%s/labels' % user_id, + f"https://api.sift.com/v203/users/{user_id}/labels", headers=mock.ANY, timeout=mock.ANY, params={}, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - - def test_unicode_string_parameter_support(self): - # str is unicode in python 3, so no need to check as this was covered - # by other unit tests. - if sys.version_info[0] < 3: - mock_response = mock.Mock() - mock_response.content = '{"status": 0, "error_message": "OK"}' - mock_response.json.return_value = json.loads(mock_response.content) - mock_response.status_code = 200 - mock_response.headers = response_with_data_header() - - user_id = '23056' - - with mock.patch.object(self.sift_client.session, 'post') as mock_post: - mock_post.return_value = mock_response - assert( - self.sift_client.track( - '$transaction', - valid_transaction_properties())) - assert( - self.sift_client.label( - user_id, - valid_label_properties())) - with mock.patch.object(self.sift_client.session, 'get') as mock_get: - mock_get.return_value = mock_response - assert(self.sift_client.score(user_id)) - - def test_unlabel_user_with_special_chars_ok(self): + assert response.is_ok() + + def test_unlabel_user_with_special_chars_ok(self) -> None: user_id = "54321=.-_+@:&^%!$" mock_response = mock.Mock() mock_response.status_code = 204 - with mock.patch.object(self.sift_client_v204.session, 'delete') as mock_delete: + + with mock.patch.object( + self.sift_client_v204.session, "delete" + ) as mock_delete: mock_delete.return_value = mock_response - response = self.sift_client_v204.unlabel(user_id, version='203') + response = self.sift_client_v204.unlabel(user_id, version="203") + mock_delete.assert_called_with( - 'https://api.siftscience.com/v203/users/%s/labels' % urllib.parse.quote(user_id), + f"https://api.sift.com/v203/users/{_q(user_id)}/labels", headers=mock.ANY, timeout=mock.ANY, params={}, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) + self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) + assert response.is_ok() - def test_label_user__with_special_chars_ok(self): - user_id = '54321=.-_+@:&^%!$' + def test_label_user__with_special_chars_ok(self) -> None: + user_id = "54321=.-_+@:&^%!$" mock_response = mock.Mock() mock_response.content = '{"status": 0, "error_message": "OK"}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response + response = self.sift_client.label( - user_id, valid_label_properties()) + user_id, valid_label_properties() + ) + properties = { - '$description': 'Listed a fake item', - '$is_bad': True, - '$reasons': ["$fake"], - '$source': 'Internal Review Queue', - '$analyst': 'super.sleuth@example.com' + "$description": "Listed a fake item", + "$is_bad": True, + "$reasons": ["$fake"], + "$source": "Internal Review Queue", + "$analyst": "super.sleuth@example.com", + "$api_key": self.test_key, + "$type": "$label", } - properties.update({'$api_key': self.test_key, '$type': '$label'}) - data = json.dumps(properties) + mock_post.assert_called_with( - 'https://api.siftscience.com/v203/users/%s/labels' % urllib.parse.quote(user_id), - data=data, + f"https://api.sift.com/v203/users/{_q(user_id)}/labels", + data=json.dumps(properties), headers=mock.ANY, timeout=mock.ANY, - params={}) + params={}, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_score__with_special_user_id_chars_ok(self): - user_id = '54321=.-_+@:&^%!$' + def test_score__with_special_user_id_chars_ok(self) -> None: + user_id = "54321=.-_+@:&^%!$" mock_response = mock.Mock() mock_response.content = score_response_json() mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.return_value = mock_response + response = self.sift_client.score(user_id) + mock_get.assert_called_with( - 'https://api.siftscience.com/v203/score/%s' % urllib.parse.quote(user_id), + f"https://api.sift.com/v203/score/{_q(user_id)}", params={}, headers=mock.ANY, timeout=mock.ANY, - auth=HTTPBasicAuth(self.test_key, '')) + auth=HTTPBasicAuth(self.test_key, ""), + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_error_message == "OK") - assert(response.body['score'] == 0.55) + assert response.is_ok() + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) + assert response.body["score"] == 0.55 - def test_exception_during_track_call(self): + def test_exception_during_track_call(self) -> None: warnings.simplefilter("always") - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.side_effect = mock.Mock( - side_effect=requests.exceptions.RequestException("Failed")) + side_effect=RequestException("Failed") + ) self.assertRaises( - sift.client.ApiException, self.sift_client.track, - '$transaction', valid_transaction_properties()) + sift.client.ApiException, + self.sift_client.track, + "$transaction", + valid_transaction_properties(), + ) - def test_exception_during_score_call(self): + def test_exception_during_score_call(self) -> None: warnings.simplefilter("always") - with mock.patch.object(self.sift_client.session, 'get') as mock_get: + + with mock.patch.object(self.sift_client.session, "get") as mock_get: mock_get.side_effect = mock.Mock( - side_effect=requests.exceptions.RequestException("Failed")) + side_effect=RequestException("Failed") + ) self.assertRaises( - sift.client.ApiException, self.sift_client.score, 'Fred') + sift.client.ApiException, self.sift_client.score, "Fred" + ) - def test_exception_during_unlabel_call(self): + def test_exception_during_unlabel_call(self) -> None: warnings.simplefilter("always") - with mock.patch.object(self.sift_client.session, 'delete') as mock_delete: + + with mock.patch.object( + self.sift_client.session, "delete" + ) as mock_delete: mock_delete.side_effect = mock.Mock( - side_effect=requests.exceptions.RequestException("Failed")) + side_effect=RequestException("Failed") + ) self.assertRaises( - sift.client.ApiException, self.sift_client.unlabel, 'Fred') + sift.client.ApiException, self.sift_client.unlabel, "Fred" + ) - def test_return_actions_on_track(self): - event = '$transaction' + def test_return_actions_on_track(self) -> None: + event = "$transaction" mock_response = mock.Mock() - mock_response.content = ('{"status": 0, "error_message": "OK", "score_response": %s}' - % action_response_json()) + mock_response.content = f'{{"status": 0, "error_message": "OK", "score_response": {action_response_json()}}}' mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() - with mock.patch.object(self.sift_client.session, 'post') as mock_post: + with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response response = self.sift_client.track( - event, valid_transaction_properties(), return_action=True) + event, valid_transaction_properties(), return_action=True + ) + mock_post.assert_called_with( - 'https://api.siftscience.com/v203/events', + "https://api.sift.com/v203/events", data=mock.ANY, headers=mock.ANY, timeout=mock.ANY, - params={'return_action': 'true'}) + params={"return_action": "true"}, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" + assert isinstance(response.body, dict) - actions = response.body["score_response"]['actions'] - assert(actions) - assert(actions[0]['action']) - assert(actions[0]['action']['id'] == 'freds_action') - assert(actions[0]['triggers']) + actions = response.body["score_response"]["actions"] + assert actions + assert actions[0]["action"] + assert actions[0]["action"]["id"] == "freds_action" + assert actions[0]["triggers"] -def main(): +def main() -> None: unittest.main() -if __name__ == '__main__': + +if __name__ == "__main__": main() diff --git a/tests/test_verification_apis.py b/tests/test_verification_apis.py index af56011..ad799b6 100644 --- a/tests/test_verification_apis.py +++ b/tests/test_verification_apis.py @@ -1,14 +1,13 @@ -from decimal import Decimal -import unittest -import warnings +from __future__ import annotations + import json -import mock +import typing as t +from unittest import TestCase, mock + import sift -import sys -import requests.exceptions -def valid_verification_send_properties(): +def valid_verification_send_properties() -> dict[str, t.Any]: return { "$user_id": "billy_jones_301", "$send_to": "billy_jones_301@gmail.com", @@ -24,39 +23,43 @@ def valid_verification_send_properties(): "$ip": "192.168.1.1", "$browser": { "$user_agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36" - } + }, + }, } -} -def valid_verification_resend_properties(): + +def valid_verification_resend_properties() -> dict[str, t.Any]: return { "$user_id": "billy_jones_301", "$verified_event": "$login", - "$verified_entity_id": "SOME_SESSION_ID" + "$verified_entity_id": "SOME_SESSION_ID", } -def valid_verification_check_properties(): + +def valid_verification_check_properties() -> dict[str, t.Any]: return { "$user_id": "billy_jones_301", "$code": "123456", "$verified_event": "$login", - "$verified_entity_id": "SOME_SESSION_ID" + "$verified_entity_id": "SOME_SESSION_ID", } -def response_with_data_header(): + +def response_with_data_header() -> dict[str, t.Any]: return { "content-length": 1, - "content-type": "application/json; charset=UTF-8" + "content-type": "application/json; charset=UTF-8", } -class TestVerificationAPI(unittest.TestCase): - def setUp(self): + +class TestVerificationAPI(TestCase): + def setUp(self) -> None: self.test_key = "a_fake_test_api_key" self.sift_client = sift.Client(self.test_key) - def test_verification_send_ok(self): + def test_verification_send_ok(self) -> None: mock_response = mock.Mock() - + send_response_json = """ { "status": 0, @@ -70,29 +73,32 @@ def test_verification_send_ok(self): "http_status_code": 200 } """ - + mock_response.content = send_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.verification_send(valid_verification_send_properties()) + response = self.sift_client.verification_send( + valid_verification_send_properties() + ) data = json.dumps(valid_verification_send_properties()) mock_post.assert_called_with( "https://api.sift.com/v1/verification/send", auth=mock.ANY, data=data, headers=mock.ANY, - timeout=mock.ANY) + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" - def test_verification_resend_ok(self): + def test_verification_resend_ok(self) -> None: mock_response = mock.Mock() - + resend_response_json = """ { "status": 0, @@ -106,29 +112,32 @@ def test_verification_resend_ok(self): "http_status_code": 200 } """ - + mock_response.content = resend_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.verification_resend(valid_verification_resend_properties()) + response = self.sift_client.verification_resend( + valid_verification_resend_properties() + ) data = json.dumps(valid_verification_resend_properties()) mock_post.assert_called_with( "https://api.sift.com/v1/verification/resend", auth=mock.ANY, data=data, headers=mock.ANY, - timeout=mock.ANY) + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") - - def test_verification_check_ok(self): + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" + + def test_verification_check_ok(self) -> None: mock_response = mock.Mock() - + check_response_json = """ { "status": 0, @@ -137,28 +146,33 @@ def test_verification_check_ok(self): "http_status_code": 200 } """ - + mock_response.content = check_response_json mock_response.json.return_value = json.loads(mock_response.content) mock_response.status_code = 200 mock_response.headers = response_with_data_header() with mock.patch.object(self.sift_client.session, "post") as mock_post: mock_post.return_value = mock_response - response = self.sift_client.verification_check(valid_verification_check_properties()) + response = self.sift_client.verification_check( + valid_verification_check_properties() + ) data = json.dumps(valid_verification_check_properties()) mock_post.assert_called_with( "https://api.sift.com/v1/verification/check", auth=mock.ANY, data=data, headers=mock.ANY, - timeout=mock.ANY) + timeout=mock.ANY, + ) self.assertIsInstance(response, sift.client.Response) - assert(response.is_ok()) - assert(response.api_status == 0) - assert(response.api_error_message == "OK") + assert response.is_ok() + assert response.api_status == 0 + assert response.api_error_message == "OK" + + +def main() -> None: + main() -def main(): - unittest.main() if __name__ == "__main__": main() From af74d5a525f3c54ec72e9a1f708bd2bf313fef85 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Tue, 25 Mar 2025 15:07:01 +0100 Subject: [PATCH 02/16] Minor fixes and remove TODO --- .flake8 | 2 +- .github/workflows/publishing2PyPI.yml | 2 +- sift/__init__.py | 6 ++++-- sift/client.py | 16 ++++------------ sift/version.py | 2 +- tests/test_client.py | 7 +++++-- 6 files changed, 16 insertions(+), 19 deletions(-) diff --git a/.flake8 b/.flake8 index 321faca..ef6c702 100644 --- a/.flake8 +++ b/.flake8 @@ -2,4 +2,4 @@ ignore = E501,W503 per-file-ignores = __init__.py:F401 max-line-length = 79 -disable-noqa = true \ No newline at end of file +disable-noqa = true diff --git a/.github/workflows/publishing2PyPI.yml b/.github/workflows/publishing2PyPI.yml index b9b2c83..0b63ba4 100644 --- a/.github/workflows/publishing2PyPI.yml +++ b/.github/workflows/publishing2PyPI.yml @@ -18,7 +18,7 @@ jobs: VERSION=$(cat ./sift/version.py | grep -E -i '^VERSION.*' | cut -d'=' -f2 | cut -d\' -f2) [[ $VERSION == "NOT_SET" ]] && echo "Version in version.py NOT_SET" && exit 1 echo "curr_version=$(echo $VERSION)" >> $GITHUB_ENV - - name: Compare package version and Releas tag + - name: Compare package version and Release tag run: | TAG=${GITHUB_REF##*/} if [[ $TAG != *"$curr_version"* ]]; then diff --git a/sift/__init__.py b/sift/__init__.py index e9ad03f..da4b03f 100644 --- a/sift/__init__.py +++ b/sift/__init__.py @@ -1,9 +1,11 @@ from __future__ import annotations +import os + from .client import Client from .version import VERSION __version__ = VERSION -api_key: str | None = None -account_id: str | None = None +api_key: str | None = os.environ.get("API_KEY") +account_id: str | None = os.environ.get("ACCOUNT_ID") diff --git a/sift/client.py b/sift/client.py index 8b9c85b..88a4c0d 100644 --- a/sift/client.py +++ b/sift/client.py @@ -24,7 +24,6 @@ "content_abuse", "legacy", "payment_abuse", - # TODO: Ask which of the following is supported (?) "promo_abuse", "promotion_abuse", ] @@ -126,16 +125,11 @@ class Client: account_id: str def __init__( - self, # TODO: Require to pass all arguments as a keyword arguments (?) + self, api_key: str | None = None, api_url: str = API_URL, - timeout: ( - int - | float - | tuple[int | float, int | float] - | tuple[int | float, int | float] - ) = 2, - account_id: str | None = None, # TODO: Move as a second argument (?) + timeout: int | float | tuple[int | float, int | float] = 2, + account_id: str | None = None, version: str = API_VERSION, session: requests.Session | None = None, ) -> None: @@ -887,9 +881,7 @@ def get_decisions( entity_type: t.Literal["user", "order", "session", "content"], limit: int | None = None, start_from: int | None = None, - abuse_types: ( - str | None - ) = None, # TODO: Ask if here should be a Sequence[AbuseType] instead of str + abuse_types: str | None = None, timeout: int | float | tuple[int | float, int | float] | None = None, ) -> Response: """Get decisions available to the customer diff --git a/sift/version.py b/sift/version.py index e85c97b..ad368aa 100644 --- a/sift/version.py +++ b/sift/version.py @@ -1,2 +1,2 @@ -VERSION = "5.6.1" +VERSION = "6.0.0" API_VERSION = "205" diff --git a/tests/test_client.py b/tests/test_client.py index fb38a53..aad819a 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -239,7 +239,9 @@ def setUp(self) -> None: def test_global_api_key(self) -> None: # test for error if global key is undefined - self.assertRaises(TypeError, sift.Client) + with mock.patch("sift.api_key"): + self.assertRaises(TypeError, sift.Client) + sift.api_key = "a_test_global_api_key" local_api_key = "a_test_local_api_key" @@ -255,7 +257,8 @@ def test_global_api_key(self) -> None: # test that client2 is assigned a new object with global api_key assert client2.api_key == sift.api_key - def test_constructor_requires_valid_api_key(self) -> None: + @mock.patch("sift.api_key", return_value=None) + def test_constructor_requires_valid_api_key(self, _) -> None: self.assertRaises(TypeError, sift.Client, None) self.assertRaises(ValueError, sift.Client, "") From a9ba7a6b9e581d4129345ef5807376d839b2cf65 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Tue, 25 Mar 2025 15:08:10 +0100 Subject: [PATCH 03/16] Update documentation --- CHANGES.md | 7 ++-- CONTRIBUTING.md | 68 ++++++++++++++++++++++++++++++++ README.md | 103 +++++++++++++++--------------------------------- 3 files changed, 104 insertions(+), 74 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 349c7a0..0ef41ce 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,11 @@ -6.0.0 Unreleased +6.0.0 (Not released yet) ================ -- Support for Python 3.13 + +- Added support for Python 3.13 INCOMPATIBLE CHANGES INTRODUCED IN 6.0.0: -- Removed support for Python < 3.8 +- Dropped support for Python < 3.8 5.6.1 2024-10-08 - Updated implementation to use Basic Authentication instead of passing `API_KEY` as a request parameter for the following calls: diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e69de29..38ffd0d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -0,0 +1,68 @@ + +## Setting up the environment + +1. Install [pyenv](https://github.com/pyenv/pyenv?tab=readme-ov-file#installation) +2. Setup virtual environment + +```sh +# install necessary Python version +pyenv install 3.13.2 + +# create a virtual enviroment +pyenv virtualenv 3.13.2 v3.13 +pyenv activate v3.13 +``` + +3. Upgrade pip + +```sh +pip install -U pip +``` + +4. Install pre-commit + +```sh +pip install -U pre-commit +``` + +5. Install the library: + +```sh +pip install -e . +``` + +## Testing + +Before submitting a change, make sure the following commands run without +errors from the root folder of the repository: + +```sh +python -m unittest discover +``` + +## Integration testing app + +For testing the app with real calls it is possible to run the integration testing app, +it makes calls to almost all Sift public API endpoints to make sure the library integrates +well. At the moment, the app is run on every merge to master + +#### How to run it locally + +1. Add env variable `API_KEY` with the valid Api Key associated from the account + +```sh +export API_KEY="api_key" +``` + +1. Add env variable `ACCOUNT_ID` with the valid account id + +```sh +export ACCOUNT_ID="account_id" +``` + +3. Run the following under the project root folder + +```sh +# run the app +python test_integration_app/main.py +``` diff --git a/README.md b/README.md index 288a8df..aae5a89 100644 --- a/README.md +++ b/README.md @@ -10,36 +10,14 @@ APIs. ## Installation -Set up a virtual environment with virtualenv (otherwise you will need -to make the pip calls as sudo): - - virtualenv venv - source venv/bin/activate - -Get the latest released package from pip: - -Python 2: - - pip install Sift - -Python 3: - - pip3 install Sift - -or install newest source directly from GitHub: - -Python 2: - - pip install git+https://github.com/SiftScience/sift-python - -Python 3: - - pip3 install git+https://github.com/SiftScience/sift-python - +```sh +# install from PyPi +pip install Sift +``` ## Documentation -Please see [here](https://sift.com/developers/docs/python/events-api/overview) for the +Please see [here](https://developers.sift.com/docs/python/apis-overview) for the most up-to-date documentation. ## Changelog @@ -59,8 +37,7 @@ Here's an example: ```python -import json -import sift.client +import sift client = sift.Client(api_key='', account_id='') @@ -85,12 +62,17 @@ properties = { } try: - response = client.track("$transaction", properties) - if response.is_ok(): - print "Successfully tracked event" + response = client.track( + "$transaction", + properties, + ) except sift.client.ApiException: # request failed pass +else: + if response.is_ok(): + print("Successfully tracked event") + # Track a transaсtion event and receive a score with percentiles in response (sync flow). # Note: `return_score` or `return_workflow_status` must be set `True`. @@ -111,15 +93,24 @@ properties = { } try: - response = client.track("$transaction", properties, return_score=True, include_score_percentiles=True, abuse_types=["promotion_abuse", "content_abuse", "payment_abuse"]) - if response.is_ok(): - score_response = response.body["score_response"] - print(score_response) + response = client.track( + "$transaction", + properties, + return_score=True, + include_score_percentiles=True, + abuse_types=("promotion_abuse", "content_abuse", "payment_abuse"), + ) except sift.client.ApiException: # request failed pass +else: + if response.is_ok(): + score_response = response.body["score_response"] + print(score_response) -# To include `warnings` field to Events API response via calling `track()` method, set it by the `include_warnings` param: + +# In order to include `warnings` field to Events API response via calling +# `track()` method, set it by the `include_warnings` param: try: response = client.track("$transaction", properties, include_warnings=True) # ... @@ -130,12 +121,12 @@ except sift.client.ApiException: # Request a score for the user with user_id 23056 try: response = client.score(user_id) - s = json.dumps(response.body) - print s - except sift.client.ApiException: # request failed pass +else: + print(response.body) + try: # Label the user with user_id 23056 as Bad with all optional fields @@ -211,6 +202,7 @@ send_properties = { } } } + try: response = client.verification_send(send_properties) except sift.client.ApiException: @@ -241,35 +233,4 @@ try: except sift.client.ApiException: # request failed pass - -``` - -## Testing - -Before submitting a change, make sure the following commands run without -errors from the root dir of the repository: - - python -m unittest discover - python3 -m unittest discover - -## Integration testing app - -For testing the app with real calls it is possible to run the integration testing app, -it makes calls to almost all our public endpoints to make sure the library integrates -well. At the moment, the app is run on every merge to master - -#### How to run it locally - -1. Add env variable `ACCOUNT_ID` with the valid account id -2. Add env variable `API_KEY` with the valid Api Key associated from the account -3. Run the following under the project root folder -``` -# uninstall the lib from the local env (if it was installed) -pip uninstall sift - -# install the lib from the local source code -pip install ../sift-python - -# run the app -python test_integration_app/main.py ``` From aca91e9dc17081a2b8bd029b405026b6659eebfd Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Wed, 26 Mar 2025 10:39:16 +0100 Subject: [PATCH 04/16] Minor fixes --- .github/workflows/ci.yml | 1 - sift/client.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26808d3..3335195 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -28,7 +28,6 @@ jobs: python-version: "3.10.14" - name: Install test dependencies run: | - pip install mock=="${{ env.mock_version_python3 }}" pip install requests=="${{ env.requests_version_python3 }}" - name: Run tests run: | diff --git a/sift/client.py b/sift/client.py index 88a4c0d..a80b4f7 100644 --- a/sift/client.py +++ b/sift/client.py @@ -204,10 +204,12 @@ def _v3_api(self, endpoint: str) -> str: return self._api_url("v3", endpoint) def _user_agent(self, version: str | None = None) -> str: + py_version = sys.version.split(" ")[0].split(".") + return ( f"SiftScience/v{version or self.version} " f"sift-python/{VERSION} " - f"Python/{sys.version.split(' ')[0]}" + f"Python/{py_version[0]}.{py_version[1]}" ) def _headers(self, version: str | None = None) -> dict[str, str]: From 153e8785f9418cf3a216d7c758022f835b700f3e Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Wed, 26 Mar 2025 10:42:04 +0100 Subject: [PATCH 05/16] Updated changelog --- CHANGES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGES.md b/CHANGES.md index 0ef41ce..2536aa5 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,6 +2,10 @@ ================ - Added support for Python 3.13 +- Dropped support for Python < 3.8 +- Added typing annotations overall the library +- Updated doc strings with actual information +- Fixed issue when library could send requests with invalid version in the "User-Agent" header INCOMPATIBLE CHANGES INTRODUCED IN 6.0.0: From e4cc5153040f9e99820e1f5aefd71ef18338ed22 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Wed, 26 Mar 2025 11:59:34 +0100 Subject: [PATCH 06/16] Pass full version of Python to the User-Agent header --- CHANGES.md | 2 +- sift/client.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 2536aa5..3364c0a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,7 +5,7 @@ - Dropped support for Python < 3.8 - Added typing annotations overall the library - Updated doc strings with actual information -- Fixed issue when library could send requests with invalid version in the "User-Agent" header +- Fixed issue when the client could send requests with invalid version in the "User-Agent" header INCOMPATIBLE CHANGES INTRODUCED IN 6.0.0: diff --git a/sift/client.py b/sift/client.py index a80b4f7..88a4c0d 100644 --- a/sift/client.py +++ b/sift/client.py @@ -204,12 +204,10 @@ def _v3_api(self, endpoint: str) -> str: return self._api_url("v3", endpoint) def _user_agent(self, version: str | None = None) -> str: - py_version = sys.version.split(" ")[0].split(".") - return ( f"SiftScience/v{version or self.version} " f"sift-python/{VERSION} " - f"Python/{py_version[0]}.{py_version[1]}" + f"Python/{sys.version.split(' ')[0]}" ) def _headers(self, version: str | None = None) -> dict[str, str]: From 79081cc8c543025e8a2d04b4ac5616a6faad1793 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Sun, 30 Mar 2025 16:53:40 +0200 Subject: [PATCH 07/16] Change signature of .get_decisions() method --- CHANGES.md | 10 +- sift/client.py | 388 +++++++++--------- .../decisions_api/test_decisions_api.py | 5 +- tests/test_client.py | 28 +- 4 files changed, 232 insertions(+), 199 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index 3364c0a..cbc7628 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,11 +5,19 @@ - Dropped support for Python < 3.8 - Added typing annotations overall the library - Updated doc strings with actual information -- Fixed issue when the client could send requests with invalid version in the "User-Agent" header +- Fixed an issue when the client could send requests with invalid version in the "User-Agent" header +- Changed the type of the `abuse_types` parameter in the `client.get_decisions()` method INCOMPATIBLE CHANGES INTRODUCED IN 6.0.0: - Dropped support for Python < 3.8 +- Passing `abuse_types` as a comma-separated string to the `client.get_decisions()` is deprecated. + + Previously, `client.get_decisions()` method allowed to pass `abuse_types` parameter as a + comma-separated string e.g. `abuse_types="legacy,payment_abuse"`. This is deprecated now. + Starting from 6.0.0 callers must pass `abuse_types` parameter to the `client.get_decisions()` + method as a sequence of string literals e.g. `abuse_types=("legacy", "payment_abuse")`. The same + way as it passed to the other client's methods which receive `abuse_types` parameter. 5.6.1 2024-10-08 - Updated implementation to use Basic Authentication instead of passing `API_KEY` as a request parameter for the following calls: diff --git a/sift/client.py b/sift/client.py index 88a4c0d..16b4f26 100644 --- a/sift/client.py +++ b/sift/client.py @@ -46,9 +46,9 @@ def _assert_non_empty_str( def _assert_non_empty_dict(val: object, name: str) -> None: - error = f"{name} must be a non-empty dict" + error = f"{name} must be a non-empty mapping (dict)" - if not isinstance(val, dict): + if not isinstance(val, Mapping): raise TypeError(error) if not val: @@ -128,7 +128,7 @@ def __init__( self, api_key: str | None = None, api_url: str = API_URL, - timeout: int | float | tuple[int | float, int | float] = 2, + timeout: float | tuple[float, float] = 2, account_id: str | None = None, version: str = API_VERSION, session: requests.Session | None = None, @@ -140,23 +140,24 @@ def __init__( The Sift Science API key associated with your account. You can obtain it from https://console.sift.com/developer/api-keys - api_url(optional): + api_url (optional): Base URL, including scheme and host, for sending events. Defaults to 'https://api.sift.com'. - timeout(optional): - Number of seconds to wait before failing a request. + timeout (optional): + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Defaults to 2 seconds. - account_id(optional): + account_id (optional): The ID of your Sift Science account. You can obtain it from https://developers.sift.com/console/account/profile - version{optional}: + version (optional): The version of the Sift Science API to call. Defaults to the latest version. - session(optional): + session (optional): requests.Session object https://requests.readthedocs.io/en/latest/user/advanced/#session-objects """ @@ -188,21 +189,10 @@ def _get_fields_param( if include ] + @property def _auth(self) -> HTTPBasicAuth: return HTTPBasicAuth(self.api_key, "") - def _api_url(self, version: str, endpoint: str) -> str: - return f"{self.url}/{version}{endpoint}" - - def _versioned_api(self, version: str, endpoint: str) -> str: - return self._api_url(f"v{version}", endpoint) - - def _v1_api(self, endpoint: str) -> str: - return self._api_url("v1", endpoint) - - def _v3_api(self, endpoint: str) -> str: - return self._api_url("v3", endpoint) - def _user_agent(self, version: str | None = None) -> str: return ( f"SiftScience/v{version or self.version} " @@ -210,18 +200,30 @@ def _user_agent(self, version: str | None = None) -> str: f"Python/{sys.version.split(' ')[0]}" ) - def _headers(self, version: str | None = None) -> dict[str, str]: + def _default_headers(self, version: str | None = None) -> dict[str, str]: return { "User-Agent": self._user_agent(version), } def _post_headers(self, version: str | None = None) -> dict[str, str]: return { + **self._default_headers(version), "Content-type": "application/json", "Accept": "*/*", - "User-Agent": self._user_agent(version), } + def _api_url(self, version: str, endpoint: str) -> str: + return f"{self.url}/{version}{endpoint}" + + def _v1_api(self, endpoint: str) -> str: + return self._api_url("v1", endpoint) + + def _v3_api(self, endpoint: str) -> str: + return self._api_url("v3", endpoint) + + def _versioned_api(self, version: str, endpoint: str) -> str: + return self._api_url(f"v{version}", endpoint) + def _events_url(self, version: str) -> str: return self._versioned_api(version, "/events") @@ -309,10 +311,10 @@ def _validate_send_request(self, properties: Mapping[str, t.Any]) -> None: ) event = properties.get("$event") - if not isinstance(event, dict): - raise TypeError("$event must be a dict") + if not isinstance(event, Mapping): + raise TypeError("$event must be a mapping (dict)") elif not event: - raise ValueError("$event dictionary may not be empty") + raise ValueError("$event mapping (dict) may not be empty") session_id = event.get("$session_id") _assert_non_empty_str(session_id, "session_id", error_cls=ValueError) @@ -336,8 +338,7 @@ def _validate_check_request(self, properties: Mapping[str, t.Any]) -> None: user_id = properties.get("$user_id") _assert_non_empty_str(user_id, "user_id", error_cls=ValueError) - otp_code = properties.get("$code") - if otp_code is None: + if properties.get("$code") is None: raise ValueError("code is required") def _validate_apply_decision_request( @@ -373,7 +374,7 @@ def track( return_route_info: bool = False, force_workflow_run: bool = False, abuse_types: Sequence[AbuseType] | None = None, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, version: str | None = None, include_score_percentiles: bool = False, include_warnings: bool = False, @@ -394,7 +395,7 @@ def track( a custom event name (that does not start with a $). properties: - A dict of additional event-specific attributes to track. + A mapping of additional event-specific attributes to track. path: An API endpoint to make a request to. @@ -408,7 +409,7 @@ def track( Whether the API response should include actions in the response. For more information on how this works, please visit the tutorial at: - https://developers.sift.com/tutorials/formulas . + https://developers.sift.com/tutorials/formulas return_workflow_status (optional): Whether the API response should include the status of any @@ -426,13 +427,14 @@ def track( score response, and no workflow will run. abuse_types (optional): - A Sequence of abuse types, specifying for which abuse types + A sequence of abuse types, specifying for which abuse types a score should be returned (if scores were requested). If not specified, a score will be returned for every abuse_type to which you are subscribed. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. version (optional): Use a different version of the Sift Science API for this call. @@ -450,11 +452,10 @@ def track( but important enough to be fixed. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(event, "event") _assert_non_empty_dict(properties, "properties") @@ -517,7 +518,7 @@ def track( def score( self, user_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, abuse_types: Sequence[AbuseType] | None = None, version: str | None = None, include_score_percentiles: bool = False, @@ -536,10 +537,11 @@ def score( used in event calls. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. abuse_types (optional): - A Sequence of abuse types, specifying for which abuse types + A sequence of abuse types, specifying for which abuse types a score should be returned (if scores were requested). If not specified, a score will be returned for every abuse_type to which you are subscribed. @@ -553,11 +555,10 @@ def score( parameter called `fields` in the query parameter Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(user_id, "user_id") @@ -581,8 +582,8 @@ def score( response = self.session.get( url, params=params, - auth=self._auth(), - headers=self._headers(version), + auth=self._auth, + headers=self._default_headers(version), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -593,7 +594,7 @@ def score( def get_user_score( self, user_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, abuse_types: Sequence[AbuseType] | None = None, include_score_percentiles: bool = False, ) -> Response: @@ -615,10 +616,11 @@ def get_user_score( event calls. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. abuse_types (optional): - A Sequence of abuse types, specifying for which abuse types + A sequence of abuse types, specifying for which abuse types a score should be returned (if scores were requested). If not specified, a score will be returned for every abuse_type to which you are subscribed. @@ -629,11 +631,10 @@ def get_user_score( called fields in the query parameter Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(user_id, "user_id") @@ -653,8 +654,8 @@ def get_user_score( response = self.session.get( url, params=params, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -665,7 +666,7 @@ def get_user_score( def rescore_user( self, user_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, abuse_types: Sequence[AbuseType] | None = None, ) -> Response: """ @@ -683,20 +684,20 @@ def rescore_user( event calls. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. abuse_types (optional): - A Sequence of abuse types, specifying for which abuse types + A sequence of abuse types, specifying for which abuse types a score should be returned (if scores were requested). If not specified, a score will be returned for every abuse_type to which you are subscribed. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(user_id, "user_id") @@ -713,8 +714,8 @@ def rescore_user( response = self.session.post( url, params=params, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -726,7 +727,7 @@ def label( self, user_id: str, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, version: str | None = None, ) -> Response: """ @@ -743,20 +744,20 @@ def label( event calls. properties: - A dict of additional event-specific attributes to track. + A mapping of additional event-specific attributes to track. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. version (optional): Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(user_id, "user_id") @@ -774,7 +775,7 @@ def label( def unlabel( self, user_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, abuse_type: AbuseType | None = None, version: str | None = None, ) -> Response: @@ -792,7 +793,8 @@ def unlabel( event calls. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. abuse_type (optional): The abuse type for which the user should be unlabeled. @@ -802,11 +804,10 @@ def unlabel( Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(user_id, "user_id") @@ -826,8 +827,8 @@ def unlabel( response = self.session.delete( url, params=params, - auth=self._auth(), - headers=self._headers(version), + auth=self._auth, + headers=self._default_headers(version), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -838,7 +839,7 @@ def unlabel( def get_workflow_status( self, run_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Gets the status of a workflow run. @@ -847,14 +848,14 @@ def get_workflow_status( The workflow run unique identifier. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") _assert_non_empty_str(run_id, "run_id") @@ -867,8 +868,8 @@ def get_workflow_status( try: response = self.session.get( url, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -881,8 +882,8 @@ def get_decisions( entity_type: t.Literal["user", "order", "session", "content"], limit: int | None = None, start_from: int | None = None, - abuse_types: str | None = None, - timeout: int | float | tuple[int | float, int | float] | None = None, + abuse_types: Sequence[AbuseType] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Get decisions available to the customer @@ -898,28 +899,34 @@ def get_decisions( Result set offset for use in pagination [default: 0] abuse_types (optional): - comma-separated list of abuse_types used to filter returned - decisions + A sequence of abuse types, specifying by which abuse types + decisions should be filtered. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") _assert_non_empty_str(entity_type, "entity_type") - if entity_type.lower() not in ["user", "order", "session", "content"]: + if entity_type.lower() not in ("user", "order", "session", "content"): raise ValueError( "entity_type must be one of {user, order, session, content}" ) + if isinstance(abuse_types, str): + raise ValueError( + "Passing `abuse_types` as string is deprecated. " + "Expected a sequence of string literals." + ) + params: dict[str, t.Any] = { "entity_type": entity_type, } @@ -931,7 +938,7 @@ def get_decisions( params["from"] = start_from if abuse_types: - params["abuse_types"] = abuse_types + params["abuse_types"] = ",".join(abuse_types) if timeout is None: timeout = self.timeout @@ -942,8 +949,8 @@ def get_decisions( response = self.session.get( url, params=params, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -955,7 +962,7 @@ def apply_user_decision( self, user_id: str, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Apply decision to a user @@ -969,14 +976,14 @@ def apply_user_decision( time: in millis when decision was applied timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -991,7 +998,7 @@ def apply_user_decision( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(), timeout=timeout, ) @@ -1005,7 +1012,7 @@ def apply_order_decision( user_id: str, order_id: str, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Apply decision to order @@ -1024,14 +1031,14 @@ def apply_order_decision( time: in millis when decision was applied (optional) timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1051,7 +1058,7 @@ def apply_order_decision( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(), timeout=timeout, ) @@ -1063,7 +1070,7 @@ def apply_order_decision( def get_user_decisions( self, user_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Gets the decisions for a user. @@ -1072,14 +1079,14 @@ def get_user_decisions( The ID of a user. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1093,8 +1100,8 @@ def get_user_decisions( try: response = self.session.get( url, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -1105,7 +1112,7 @@ def get_user_decisions( def get_order_decisions( self, order_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Gets the decisions for an order. @@ -1114,14 +1121,14 @@ def get_order_decisions( The ID for the order. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1135,8 +1142,8 @@ def get_order_decisions( try: response = self.session.get( url, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -1148,7 +1155,7 @@ def get_content_decisions( self, user_id: str, content_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Gets the decisions for a piece of content. @@ -1160,14 +1167,14 @@ def get_content_decisions( The ID for the content. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1182,8 +1189,8 @@ def get_content_decisions( try: response = self.session.get( url, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -1195,7 +1202,7 @@ def get_session_decisions( self, user_id: str, session_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Gets the decisions for a user's session. @@ -1207,14 +1214,14 @@ def get_session_decisions( The ID for the session. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1229,8 +1236,8 @@ def get_session_decisions( try: response = self.session.get( url, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -1243,7 +1250,7 @@ def apply_session_decision( user_id: str, session_id: str, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Apply decision to a session. @@ -1262,14 +1269,14 @@ def apply_session_decision( time: in millis when decision was applied (optional) timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1287,7 +1294,7 @@ def apply_session_decision( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(), timeout=timeout, ) @@ -1301,7 +1308,7 @@ def apply_content_decision( user_id: str, content_id: str, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Apply decision to a piece of content. @@ -1320,14 +1327,14 @@ def apply_content_decision( time: in millis when decision was applied (optional) timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") _assert_non_empty_str(user_id, "user_id") @@ -1344,7 +1351,7 @@ def apply_content_decision( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(), timeout=timeout, ) @@ -1356,23 +1363,23 @@ def apply_content_decision( def create_psp_merchant_profile( self, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Create a new PSP Merchant profile Args: properties: - A dict of merchant profile data. + A mapping of merchant profile data. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1386,7 +1393,7 @@ def create_psp_merchant_profile( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(), timeout=timeout, ) @@ -1399,7 +1406,7 @@ def update_psp_merchant_profile( self, merchant_id: str, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Update already existing PSP Merchant profile @@ -1409,17 +1416,17 @@ def update_psp_merchant_profile( the good or service. properties: - A dict of merchant profile data. + A mapping of merchant profile data. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1433,7 +1440,7 @@ def update_psp_merchant_profile( response = self.session.put( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(), timeout=timeout, ) @@ -1446,7 +1453,7 @@ def get_psp_merchant_profiles( self, batch_token: str | None = None, batch_size: int | None = None, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Gets all PSP merchant profiles (paginated). @@ -1458,14 +1465,14 @@ def get_psp_merchant_profiles( Batch or page size of the paginated sequence. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1486,8 +1493,8 @@ def get_psp_merchant_profiles( try: response = self.session.get( url, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), params=params, timeout=timeout, ) @@ -1499,7 +1506,7 @@ def get_psp_merchant_profiles( def get_a_psp_merchant_profile( self, merchant_id: str, - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, ) -> Response: """Gets a PSP merchant profile by merchant id. @@ -1509,14 +1516,14 @@ def get_a_psp_merchant_profile( the good or service. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ _assert_non_empty_str(self.account_id, "account_id") @@ -1529,8 +1536,8 @@ def get_a_psp_merchant_profile( try: response = self.session.get( url, - auth=self._auth(), - headers=self._headers(), + auth=self._auth, + headers=self._default_headers(), timeout=timeout, ) except requests.exceptions.RequestException as e: @@ -1541,7 +1548,7 @@ def get_a_psp_merchant_profile( def verification_send( self, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, version: str | None = None, ) -> Response: """ @@ -1588,17 +1595,17 @@ def verification_send( Use this field if the client is a browser. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. version (optional): Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ if timeout is None: @@ -1615,7 +1622,7 @@ def verification_send( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(version), timeout=timeout, ) @@ -1627,7 +1634,7 @@ def verification_send( def verification_resend( self, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, version: str | None = None, ) -> Response: """ @@ -1642,7 +1649,6 @@ def verification_resend( Args: properties: - $user_id: User ID of user being verified, e.g. johndoe123. $verified_event (optional): @@ -1651,17 +1657,17 @@ def verification_resend( The ID of the entity impacted by the event being verified. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. version (optional): Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ if timeout is None: @@ -1678,7 +1684,7 @@ def verification_resend( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(version), timeout=timeout, ) @@ -1690,7 +1696,7 @@ def verification_resend( def verification_check( self, properties: Mapping[str, t.Any], - timeout: int | float | tuple[int | float, int | float] | None = None, + timeout: float | tuple[float, float] | None = None, version: str | None = None, ) -> Response: """ @@ -1718,17 +1724,17 @@ def verification_check( The ID of the entity impacted by the event being verified. timeout (optional): - Use a custom timeout (in seconds) for this call. + How many seconds to wait for the server to send data before + giving up, as a float, or a (connect timeout, read timeout) tuple. version (optional): Use a different version of the Sift Science API for this call. Returns: - A sift.client.Response object if the call succeeded + A sift.client.Response object if the call to the Sift API is successful Raises: - ApiException: - if the call not succeeded + ApiException: If the call to the Sift API is not successful """ if timeout is None: timeout = self.timeout @@ -1744,7 +1750,7 @@ def verification_check( response = self.session.post( url, data=json.dumps(properties), - auth=self._auth(), + auth=self._auth, headers=self._post_headers(version), timeout=timeout, ) diff --git a/test_integration_app/decisions_api/test_decisions_api.py b/test_integration_app/decisions_api/test_decisions_api.py index 0a16364..da05991 100644 --- a/test_integration_app/decisions_api/test_decisions_api.py +++ b/test_integration_app/decisions_api/test_decisions_api.py @@ -76,5 +76,8 @@ def get_decisions(self) -> sift.client.Response: entity_type="user", limit=10, start_from=5, - abuse_types="legacy,payment_abuse", + abuse_types=( + "legacy", + "payment_abuse", + ), ) diff --git a/tests/test_client.py b/tests/test_client.py index aad819a..9021b48 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -257,10 +257,10 @@ def test_global_api_key(self) -> None: # test that client2 is assigned a new object with global api_key assert client2.api_key == sift.api_key - @mock.patch("sift.api_key", return_value=None) - def test_constructor_requires_valid_api_key(self, _) -> None: - self.assertRaises(TypeError, sift.Client, None) - self.assertRaises(ValueError, sift.Client, "") + def test_constructor_requires_valid_api_key(self) -> None: + with mock.patch("sift.api_key", return_value=None): + self.assertRaises(TypeError, sift.Client, None) + self.assertRaises(ValueError, sift.Client, "") def test_constructor_invalid_api_url(self) -> None: self.assertRaises(TypeError, sift.Client, self.test_key, None) @@ -665,7 +665,10 @@ def test_get_decisions(self) -> None: entity_type="user", limit=10, start_from=None, - abuse_types="legacy,payment_abuse", + abuse_types=( + "legacy", + "payment_abuse", + ), timeout=3, ) @@ -720,7 +723,7 @@ def test_get_decisions_entity_session(self) -> None: entity_type="session", limit=10, start_from=None, - abuse_types="account_takeover", + abuse_types=("account_takeover",), timeout=3, ) @@ -740,6 +743,19 @@ def test_get_decisions_entity_session(self) -> None: assert isinstance(response.body, dict) assert response.body["data"][0]["id"] == "block_session" + def test_get_decisions_with_deprecated_signature(self) -> None: + with mock.patch.object(self.sift_client.session, "get") as mock_get: + with self.assertRaises(ValueError): + self.sift_client.get_decisions( + entity_type="session", + limit=10, + start_from=None, + abuse_types=("legacy", "account_takeover"), + timeout=3, + ) + + mock_get.assert_not_called() + def test_apply_decision_to_user_ok(self) -> None: user_id = "54321" mock_response = mock.Mock() From f13189b48af595888dd5851b2b57b1f528b0d974 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Sun, 30 Mar 2025 16:57:50 +0200 Subject: [PATCH 08/16] Change signature of .get_decisions() method --- tests/test_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_client.py b/tests/test_client.py index 9021b48..b69abee 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -750,7 +750,7 @@ def test_get_decisions_with_deprecated_signature(self) -> None: entity_type="session", limit=10, start_from=None, - abuse_types=("legacy", "account_takeover"), + abuse_types=t.cast(list, "legacy,account_takeover"), timeout=3, ) From 79d048e822c372e6219f4a08ee77ff967f0f3f21 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Mon, 28 Apr 2025 15:31:19 +0200 Subject: [PATCH 09/16] Allow abuse type to be passed as any string --- sift/client.py | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/sift/client.py b/sift/client.py index 16b4f26..79c4b32 100644 --- a/sift/client.py +++ b/sift/client.py @@ -18,16 +18,6 @@ from sift.utils import DecimalEncoder, quote_path as _q from sift.version import API_VERSION, VERSION -AbuseType = t.Literal[ - "account_abuse", - "account_takeover", - "content_abuse", - "legacy", - "payment_abuse", - "promo_abuse", - "promotion_abuse", -] - def _assert_non_empty_str( val: object, @@ -373,7 +363,7 @@ def track( return_workflow_status: bool = False, return_route_info: bool = False, force_workflow_run: bool = False, - abuse_types: Sequence[AbuseType] | None = None, + abuse_types: Sequence[str] | None = None, timeout: float | tuple[float, float] | None = None, version: str | None = None, include_score_percentiles: bool = False, @@ -519,7 +509,7 @@ def score( self, user_id: str, timeout: float | tuple[float, float] | None = None, - abuse_types: Sequence[AbuseType] | None = None, + abuse_types: Sequence[str] | None = None, version: str | None = None, include_score_percentiles: bool = False, ) -> Response: @@ -595,7 +585,7 @@ def get_user_score( self, user_id: str, timeout: float | tuple[float, float] | None = None, - abuse_types: Sequence[AbuseType] | None = None, + abuse_types: Sequence[str] | None = None, include_score_percentiles: bool = False, ) -> Response: """ @@ -667,7 +657,7 @@ def rescore_user( self, user_id: str, timeout: float | tuple[float, float] | None = None, - abuse_types: Sequence[AbuseType] | None = None, + abuse_types: Sequence[str] | None = None, ) -> Response: """ Rescores the specified user for the specified abuse types and returns @@ -776,7 +766,7 @@ def unlabel( self, user_id: str, timeout: float | tuple[float, float] | None = None, - abuse_type: AbuseType | None = None, + abuse_type: str | None = None, version: str | None = None, ) -> Response: """ @@ -882,7 +872,7 @@ def get_decisions( entity_type: t.Literal["user", "order", "session", "content"], limit: int | None = None, start_from: int | None = None, - abuse_types: Sequence[AbuseType] | None = None, + abuse_types: Sequence[str] | None = None, timeout: float | tuple[float, float] | None = None, ) -> Response: """Get decisions available to the customer From f00e56852ace01e72ecb983c74bcc2aadc2914f2 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Mon, 28 Apr 2025 15:36:42 +0200 Subject: [PATCH 10/16] Update linters --- .pre-commit-config.yaml | 2 +- requirements-dev.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 087e858..37254e3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/crate-ci/typos - rev: v1.30.2 + rev: v1.31.1 hooks: - id: typos args: [ --force-exclude ] diff --git a/requirements-dev.txt b/requirements-dev.txt index a78b232..fc40ddf 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,4 +2,4 @@ black==24.8.0 flake8==7.1.2 isort==5.13.2 mypy==1.14.1 -typos==1.30.2 +typos==1.31.1 From 3a9d5ea749555fbb9abf7c7d73f5222b9d0487ad Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Tue, 29 Apr 2025 19:40:30 +0200 Subject: [PATCH 11/16] Update CI --- .github/workflows/ci.yml | 15 +++++++++------ .github/workflows/publishing2PyPI.yml | 7 ++++--- .travis.yml | 23 ----------------------- CONTRIBUTING.md | 4 ++-- 4 files changed, 15 insertions(+), 34 deletions(-) delete mode 100644 .travis.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3335195..2e7c39e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,8 +12,6 @@ permissions: contents: read env: - mock_version_python3: "5.0.1" - requests_version_python3: "2.28.2" ACCOUNT_ID: ${{ secrets.ACCOUNT_ID }} API_KEY: ${{ secrets.API_KEY }} @@ -28,7 +26,12 @@ jobs: python-version: "3.10.14" - name: Install test dependencies run: | - pip install requests=="${{ env.requests_version_python3 }}" + pip install -e . + pip install -r requirements-dev.txt + - name: Run linters + run: | + pip install -U pre-commit + pre-commit run -v --all-files - name: Run tests run: | python -m unittest discover @@ -42,7 +45,7 @@ jobs: uses: actions/setup-python@v3 with: python-version: "3.10.14" - - name: run-integration-tests-python3 + - name: Run integration tests run: | - pip3 install . - python3 test_integration_app/main.py + pip install . + python test_integration_app/main.py diff --git a/.github/workflows/publishing2PyPI.yml b/.github/workflows/publishing2PyPI.yml index 0b63ba4..810a8f4 100644 --- a/.github/workflows/publishing2PyPI.yml +++ b/.github/workflows/publishing2PyPI.yml @@ -27,11 +27,12 @@ jobs: fi - name: Create distribution files run: | - python3 setup.py sdist + python -m pip install build + python -m build - name: Upload distribution files env: TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} TWINE_USER: ${{ secrets.USER }} run: | - python3 -m pip install --user --upgrade twine - ls dist/ | xargs -I % python3 -m twine upload --repository pypi dist/% + python -m pip install --user --upgrade twine + ls dist/ | xargs -I % python -m twine upload --repository pypi dist/% diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index ff6e263..0000000 --- a/.travis.yml +++ /dev/null @@ -1,23 +0,0 @@ -language: python -python: - - "3.8" - - "3.9" - - "3.10" - - "3.11" - - "3.12" - - "3.13" -before_install: - - python --version - - pip install -U pip -# command to install dependencies -install: - - pip install -e . - - pip install -r requirements-dev.txt -# command to run tests -script: - - flake8 --count - - black --check . - - isort --check . - - mypy --install-types --non-interactive . - - typos - - python -m unittest discover diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 38ffd0d..c53bb5e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,9 +6,9 @@ ```sh # install necessary Python version -pyenv install 3.13.2 +pyenv install 3.13.2 -# create a virtual enviroment +# create a virtual environment pyenv virtualenv 3.13.2 v3.13 pyenv activate v3.13 ``` From 324776ea275443c2a6cd66611aac7f4bae0d4478 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Tue, 29 Apr 2025 19:41:31 +0200 Subject: [PATCH 12/16] Remove trailing space --- CONTRIBUTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c53bb5e..b8db25a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,7 +6,7 @@ ```sh # install necessary Python version -pyenv install 3.13.2 +pyenv install 3.13.2 # create a virtual environment pyenv virtualenv 3.13.2 v3.13 From 6533609847aa3f5b19ca76678c79e3b56ebd76ab Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Tue, 29 Apr 2025 19:49:08 +0200 Subject: [PATCH 13/16] Install requirements from pre-commit config --- .github/workflows/ci.yml | 3 +-- requirements-dev.txt | 5 ----- 2 files changed, 1 insertion(+), 7 deletions(-) delete mode 100644 requirements-dev.txt diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2e7c39e..cff872f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,10 +24,9 @@ jobs: uses: actions/setup-python@v3 with: python-version: "3.10.14" - - name: Install test dependencies + - name: Install the library run: | pip install -e . - pip install -r requirements-dev.txt - name: Run linters run: | pip install -U pre-commit diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index fc40ddf..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1,5 +0,0 @@ -black==24.8.0 -flake8==7.1.2 -isort==5.13.2 -mypy==1.14.1 -typos==1.31.1 From 4b46a84866f29ebc6fe768bb934cee44a4051566 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Tue, 29 Apr 2025 20:05:04 +0200 Subject: [PATCH 14/16] Add missing command --- CONTRIBUTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b8db25a..e583866 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -23,6 +23,7 @@ pip install -U pip ```sh pip install -U pre-commit +pre-commit install ``` 5. Install the library: From b5a13663adeace8110357b5d98fe61231c7d01e0 Mon Sep 17 00:00:00 2001 From: Denys Pecheniev Date: Wed, 30 Apr 2025 12:33:49 +0200 Subject: [PATCH 15/16] Update links to the documentation --- README.md | 12 +++--------- sift/client.py | 14 +++++++------- 2 files changed, 10 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index aae5a89..5bc94c5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,12 @@ # Sift Python Bindings Bindings for Sift's APIs -- including the -[Events](https://sift.com/resources/references/events-api.html), -[Labels](https://sift.com/resources/references/labels-api.html), +[Events](https://developers.sift.com/docs/python/events-api/, +[Labels](https://developers.sift.com/docs/python/labels-api/), and -[Score](https://sift.com/resources/references/score-api.html) +[Score](https://developers.sift.com/docs/python/score-api/) APIs. - ## Installation ```sh @@ -26,11 +25,6 @@ Please see [the CHANGELOG](https://github.com/SiftScience/sift-python/blob/master/CHANGES.md) for a history of all changes. -Note, that in v2.0.0, the API semantics were changed to raise an -exception in the case of error to be more pythonic. Client code will -need to be updated to catch `sift.client.ApiException` exceptions. - - ## Usage Here's an example: diff --git a/sift/client.py b/sift/client.py index 79c4b32..b421ce4 100644 --- a/sift/client.py +++ b/sift/client.py @@ -1,5 +1,5 @@ """Python client for Sift Science's API. -See: https://developers.sift.com/docs/python/events-api +See: https://developers.sift.com/docs/python/events-api/ """ from __future__ import annotations @@ -374,7 +374,7 @@ def track( This call is blocking. - Visit https://siftscience.com/resources/references/events-api + Visit https://developers.sift.com/docs/python/events-api/ for more information on what types of events you can send and fields you can add to the properties parameter. @@ -518,7 +518,7 @@ def score( This call is blocking. - Visit https://developers.sift.com/docs/python/score-api + Visit https://developers.sift.com/docs/python/score-api/ for more details on our Score response structure. Args: @@ -597,7 +597,7 @@ def get_user_score( This call is blocking. - Visit https://developers.sift.com/docs/python/score-api/get-score + Visit https://developers.sift.com/docs/python/score-api/get-score/ for more details. Args: @@ -665,7 +665,7 @@ def rescore_user( This call is blocking. - Visit https://developers.sift.com/docs/python/score-api/rescore/overview + Visit https://developers.sift.com/docs/python/score-api/rescore/ for more details. Args: @@ -725,7 +725,7 @@ def label( This call is blocking. - Visit https://developers.sift.com/docs/python/labels-api + Visit https://developers.sift.com/docs/python/labels-api/label-user for more details on what fields to send in properties. Args: @@ -774,7 +774,7 @@ def unlabel( This call is blocking. - Visit https://developers.sift.com/docs/python/labels-api + Visit https://developers.sift.com/docs/python/labels-api/unlabel-user for more details. Args: From 3c2984f6a06bc9ab8d6873e6ba4b20c832547722 Mon Sep 17 00:00:00 2001 From: Eduard Chumak <71296844+echumak-sift@users.noreply.github.com> Date: Mon, 5 May 2025 13:47:14 -0700 Subject: [PATCH 16/16] Update CHANGES.md Updated release date --- CHANGES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index cbc7628..31c250b 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,4 +1,4 @@ -6.0.0 (Not released yet) +6.0.0 2025-05-05 ================ - Added support for Python 3.13