diff --git a/README.md b/README.md index 5ed5d46..8e4f13a 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,4 @@ If you use a Telesign SDK to make your request, authentication is handled behind * Learn to send a request to Telesign with code with one of our [tutorials](https://developer.telesign.com/enterprise/docs/tutorials). * Browse our [Developer Portal](https://developer.telesign.com) for tutorials, how-to guides, reference content, and more. -* Check out our [sample code](https://github.com/TeleSign/sample_code) on GitHub. - - +* Check out our [sample code](https://github.com/TeleSign/sample_code) on GitHub. \ No newline at end of file diff --git a/RELEASE b/RELEASE index 3e22161..7e4571c 100644 --- a/RELEASE +++ b/RELEASE @@ -1,3 +1,31 @@ +3.0.1 +- Removed deprecated pkg_resources.declare_namespace() usage. +- Cleaned up old mock dependency logic. +- Updated classifiers to Python 3.7–3.12. + +3.0.0 +- Removed all functionality and methods for the Legacy Intelligence Use Case API +- Removed all functionality and methods for App Verify SDK + +2.3.1 +- Adds automatic HTTP session recycling (`pool_recycle`). +- Improves keep-alive connection handling. + +2.3.0 +- Added PATCH method to the RestClient class to facilitate Update Verification Process action + +2.2.7 +- Added tracking to requests + +2.2.6 +- Add setters for URL + +2.2.5 +- Fixing auth issue that was causing requests to fail + +2.2.4 +- Resolved an issue causing PhoneID requests to fail + 2.2.3 - Added support for Intelligence(basic auth) API diff --git a/examples/appverify/1_get_status_by_external_id.py b/examples/appverify/1_get_status_by_external_id.py deleted file mode 100644 index 3969fb7..0000000 --- a/examples/appverify/1_get_status_by_external_id.py +++ /dev/null @@ -1,16 +0,0 @@ -from __future__ import print_function -from telesign.appverify import AppVerifyClient - -customer_id = "FFFFFFFF-EEEE-DDDD-1234-AB1234567890" -api_key = "EXAMPLE----TE8sTgg45yusumoN6BYsBVkh+yRJ5czgsnCehZaOYldPJdmFh6NeX8kunZ2zU1YWaUw/0wV6xfw==" - -external_id = "external_id" - -appverify = AppVerifyClient(customer_id, api_key) -response = appverify.status(external_id) - -if response.ok: - print("App Verify transaction with external_id {} has status code {} and status description {}.".format( - external_id, - response.json['status']['code'], - response.json['status']['description'])) diff --git a/examples/intelligence/1_get_risk_score_and_related_insights.py b/examples/intelligence/1_get_risk_score_and_related_insights.py deleted file mode 100644 index 1acb6f2..0000000 --- a/examples/intelligence/1_get_risk_score_and_related_insights.py +++ /dev/null @@ -1,29 +0,0 @@ -"""Example code that makes requests to intelligence API.""" -from __future__ import print_function -from telesign.intelligence import IntelligenceClient - -customer_1 = "your_customer_id-44ZA-47B5-95B9-ACXM9B1E5CAA" -api_key_1 = "your_api_key_or_password" - -phone_number = "15555551212" -account_lifecycle_event = "create" - -body = { - "contact_details": {"email": "ghopper@gmail.com", "phone_number": "15555551212"}, - "external_id": "REG432538", - "account_lifecycle_event": "create", - "ip": "1.1.1.1", - "device_id": "2e4fa042234d", -} - -client = IntelligenceClient(customer_1, api_key_1) -response = client.intelligence(body) - -if response.ok: - print( - "Phone number {} has a '{}' risk level and the score is '{}'.".format( - response.json["phone_details"]["numbering"]["original"]["phone_number"], - response.json["risk"]["level"], - response.json["risk"]["score"], - ) - ) diff --git a/setup.py b/setup.py index 81cac1a..a74ee63 100644 --- a/setup.py +++ b/setup.py @@ -6,12 +6,9 @@ EXCLUDE_FROM_PACKAGES = ['tests'] -needs_mock = sys.version_info < (3, 3) -mock = ['mock'] if needs_mock else [] - here = path.abspath(path.dirname(__file__)) -version = "2.2.6" +version = "3.0.1" with open(path.join(here, 'README.rst'), encoding='utf-8') as f: long_description = f.read() @@ -34,6 +31,12 @@ "Programming Language :: Python :: 3.4", "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "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", ], long_description=long_description, keywords='telesign, sms, voice, mobile, authentication, identity, messaging', @@ -42,6 +45,6 @@ url="https://github.com/telesign/python_telesign", install_requires=['requests'], test_suite='nose.collector', - tests_require=['nose', 'pytz'] + mock, + tests_require=['nose', 'pytz'], packages=find_packages(exclude=EXCLUDE_FROM_PACKAGES), ) diff --git a/telesign/__init__.py b/telesign/__init__.py index 484190b..dd71b4b 100644 --- a/telesign/__init__.py +++ b/telesign/__init__.py @@ -1,7 +1,5 @@ from pkg_resources import get_distribution -__import__('pkg_resources').declare_namespace(__name__) - __version__ = get_distribution("telesign").version __author__ = "TeleSign" __copyright__ = "Copyright 2017, TeleSign Corp." diff --git a/telesign/appverify.py b/telesign/appverify.py deleted file mode 100644 index 7a41e60..0000000 --- a/telesign/appverify.py +++ /dev/null @@ -1,28 +0,0 @@ -from __future__ import unicode_literals - -from telesign.rest import RestClient - -APPVERIFY_STATUS_RESOURCE = "/v1/mobile/verification/status/{external_id}" - - -class AppVerifyClient(RestClient): - """ - App Verify is a secure, lightweight SDK that integrates a frictionless user verification process into existing - native mobile applications. - """ - - def __init__(self, customer_id, api_key, **kwargs): - super(AppVerifyClient, self).__init__(customer_id, api_key, **kwargs) - - def status(self, external_id, **params): - """ - Retrieves the verification result for an App Verify transaction by external_id. To ensure a secure verification - flow you must check the status using TeleSign's servers on your backend. Do not rely on the SDK alone to - indicate a successful verification. - - See https://developer.telesign.com/docs/app-verify-android-sdk-self#section-get-status-service or - https://developer.telesign.com/docs/app-verify-ios-sdk-self#section-get-status-service for detailed - API documentation. - """ - return self.get(APPVERIFY_STATUS_RESOURCE.format(external_id=external_id), - **params) diff --git a/telesign/intelligence.py b/telesign/intelligence.py deleted file mode 100644 index b44ed6a..0000000 --- a/telesign/intelligence.py +++ /dev/null @@ -1,39 +0,0 @@ -"""Client to make requests to intelligence API.""" -from __future__ import unicode_literals - -from telesign.rest import RestClient -from telesign.util import AuthMethod - -INTELLIGENCE_BASE_URL = "https://detect.telesign.com" -INTELLIGENCE_ENDPOINT_PATH = "/intelligence" - - -class IntelligenceClient(RestClient): - """ - It is critical today to evaluate fraud risk throughout the entire customer journey. - - Telesign Intelligence makes it easy to understand the risk and the reason behind it with tailored scoring models - and comprehensive reason codes. - """ - - def __init__(self, customer_id, api_key, **kwargs): - super(IntelligenceClient, self).__init__( - customer_id=customer_id, - api_key=api_key, - rest_endpoint=INTELLIGENCE_BASE_URL, - auth_method=AuthMethod.BASIC.value, - **kwargs - ) - - def intelligence(self, params): - """ - Telesign Intelligence is like a credit check for digital profiles. - - You submit a phone number, IP, and email to the service, the individual - identifiers are each evaluated, and then a score is returned telling you how risky - that user is. You decide whether to proceed based on the score. - - See https://developer.telesign.com/enterprise/docs/intelligence-overview - for detailed API documentation. - """ - return self.post(INTELLIGENCE_ENDPOINT_PATH, body=params, query_params=None) diff --git a/telesign/rest.py b/telesign/rest.py index 4ca77c6..b147cd0 100644 --- a/telesign/rest.py +++ b/telesign/rest.py @@ -6,6 +6,7 @@ from email.utils import formatdate from hashlib import sha256 from platform import python_version +import time import requests import json @@ -16,18 +17,14 @@ class RestClient(requests.models.RequestEncodingMixin): """ - The TeleSign RestClient is a generic HTTP REST client that can be extended to make requests against any of - TeleSign's REST API endpoints. + The Telesign RestClient is a generic HTTP REST client that can be extended to make requests against any of + Telesign's REST API endpoints. RequestEncodingMixin offers the function _encode_params for url encoding the body for use in string_to_sign outside of a regular HTTP request. See https://developer.telesign.com for detailed API documentation. """ - user_agent = "TeleSignSDK/python-{sdk_version} Python/{python_version} Requests/{requests_version}".format( - sdk_version=telesign.__version__, - python_version=python_version(), - requests_version=requests.__version__) class Response(object): """ @@ -51,11 +48,15 @@ def __init__(self, customer_id, api_key, rest_endpoint='https://rest-api.telesign.com', + source="python_telesign", + sdk_version_origin=None, + sdk_version_dependency=None, proxies=None, timeout=10, - auth_method=None): + auth_method=None, + pool_recycle=480): """ - TeleSign RestClient useful for making generic RESTful requests against our API. + Telesign RestClient useful for making generic RESTful requests against our API. :param customer_id: Your customer_id string associated with your account. :param api_key: Your api_key string associated with your account. @@ -63,13 +64,22 @@ def __init__(self, :param proxies: (optional) Dictionary mapping protocol or protocol and hostname to the URL of the proxy. :param timeout: (optional) How long to wait for the server to send data before giving up, as a float, or as a (connect timeout, read timeout) tuple + :param pool_recycle: (optional) Time in seconds to recycle the HTTP session to avoid stale connections (default 480). + If a session is older than this value, it will be closed and a new session will be created automatically before each request. + This helps prevent errors due to HTTP keep-alive connections being closed by the server after inactivity. + + HTTP Keep-Alive behavior: + TeleSign endpoints close idle HTTP keep-alive connections after 499 seconds. If you attempt to reuse a connection older than this, you may get a 'connection reset by peer' error. + By default, pool_recycle=480 ensures sessions are refreshed before this limit. """ self.customer_id = customer_id self.api_key = api_key self.api_host = rest_endpoint - self.session = requests.Session() + self.pool_recycle = pool_recycle + self._session_created_at = None + self.session = self._create_session() self.session.proxies = proxies if proxies else {} @@ -77,6 +87,17 @@ def __init__(self, self.auth_method = auth_method + current_version_sdk = telesign.__version__ if source == "python_telesign" else sdk_version_origin + + self.user_agent = "TeleSignSDK/python Python/{python_version} Requests/{requests_version} OriginatingSDK/{source} SDKVersion/{sdk_version}".format( + python_version=python_version(), + requests_version=requests.__version__, + source=source, + sdk_version=current_version_sdk) + + if source != "python_telesign": + self.user_agent = self.user_agent + " DependencySDKVersion/{sdk_version_dependency}".format(sdk_version_dependency=sdk_version_dependency) + @staticmethod def generate_telesign_headers(customer_id, api_key, @@ -89,17 +110,17 @@ def generate_telesign_headers(customer_id, content_type=None, auth_method=None): """ - Generates the TeleSign REST API headers used to authenticate requests. + Generates the Telesign REST API headers used to authenticate requests. Creates the canonicalized string_to_sign and generates the HMAC signature. This is used to authenticate requests - against the TeleSign REST API. + against the Telesign REST API. See https://developer.telesign.com/docs/authentication for detailed API documentation. :param customer_id: Your account customer_id. :param api_key: Your account api_key. :param method_name: The HTTP method name of the request as a upper case string, should be one of 'POST', 'GET', - 'PUT' or 'DELETE'. + 'PUT', 'PATCH' or 'DELETE'. :param resource: The partial resource URI to perform the request against, as a string. :param url_encoded_fields: HTTP body parameters to perform the HTTP request with, must be a urlencoded string. :param date_rfc2616: The date and time of the request formatted in rfc 2616, as a string. @@ -107,7 +128,7 @@ def generate_telesign_headers(customer_id, :param user_agent: (optional) User Agent associated with the request, as a string. :param content_type: (optional) ContentType of the request, as a string. :param auth_method: (optional) Authentication type ex: Basic, HMAC etc - :return: The TeleSign authentication headers. + :return: The Telesign authentication headers. """ if date_rfc2616 is None: date_rfc2616 = formatdate(usegmt=True) @@ -167,7 +188,7 @@ def generate_telesign_headers(customer_id, def post(self, resource, body=None, json_fields=None, **query_params): """ - Generic TeleSign REST API POST handler. + Generic Telesign REST API POST handler. :param resource: The partial resource URI to perform the request against, as a string. :param body: (optional) A dictionary sent as a part of request body. @@ -178,7 +199,7 @@ def post(self, resource, body=None, json_fields=None, **query_params): def get(self, resource, body=None, json_fields=None, **query_params): """ - Generic TeleSign REST API GET handler. + Generic Telesign REST API GET handler. :param resource: The partial resource URI to perform the request against, as a string. :param body: (optional) A dictionary sent as a part of request body. @@ -189,7 +210,7 @@ def get(self, resource, body=None, json_fields=None, **query_params): def put(self, resource, body=None, json_fields=None, **query_params): """ - Generic TeleSign REST API PUT handler. + Generic Telesign REST API PUT handler. :param resource: The partial resource URI to perform the request against, as a string. :param body: (optional) A dictionary sent as a part of request body. @@ -203,7 +224,7 @@ def set_endpoint(self, rest_endpoint): def delete(self, resource, body=None, json_fields=None, **query_params): """ - Generic TeleSign REST API DELETE handler. + Generic Telesign REST API DELETE handler. :param resource: The partial resource URI to perform the request against, as a string. :param body: (optional) A dictionary sent as a part of request body. @@ -211,10 +232,33 @@ def delete(self, resource, body=None, json_fields=None, **query_params): :return: The RestClient Response object. """ return self._execute(self.session.delete, 'DELETE', resource, body, json_fields, **query_params) + + def patch(self, resource, body=None, json_fields=None, **query_params): + """ + Generic Telesign REST API PATCH handler. + + :param resource: The partial resource URI to perform the request against, as a string. + :param body: (optional) A dictionary sent as a part of request body. + :param json_fields: (optional) A dictionary sent as a JSON body. + :param query_params: query_params to perform the PATCH request with, as a dictionary. + :return: The RestClient Response object. + """ + return self._execute(self.session.patch, 'PATCH', resource, body, json_fields, **query_params) + + def _create_session(self): + session = requests.Session() + self._session_created_at = time.time() + return session + + def _ensure_session(self): + if self._session_created_at is None or (time.time() - self._session_created_at > self.pool_recycle): + if self.session: + self.session.close() + self.session = self._create_session() def _execute(self, method_function, method_name, resource, body=None, json_fields=None, **query_params): """ - Generic TeleSign REST API request handler. + Generic Telesign REST API request handler. :param method_function: The Requests HTTP request function to perform the request. :param method_name: The HTTP method name, as an upper case string. @@ -223,6 +267,7 @@ def _execute(self, method_function, method_name, resource, body=None, json_field :param query_params: query_params to perform the HTTP request with, as a dictionary. :return: The RestClient Response object. """ + self._ensure_session() resource_uri = "{api_host}{resource}".format(api_host=self.api_host, resource=resource) url_encoded_fields = self._encode_params(query_params) diff --git a/tests/test_phoneid.py b/tests/test_phoneid.py index 3a45411..74fb964 100644 --- a/tests/test_phoneid.py +++ b/tests/test_phoneid.py @@ -17,25 +17,6 @@ def test_phoneid_constructor(self): self.assertEqual(client.customer_id, self.customer_id) self.assertEqual(client.api_key, self.api_key) - - def test_phoneid_pid_contact(self): - - client = PhoneIdClient(self.customer_id, self.api_key) - content_type_expected = 'application/json' - status_code_expected = 200 - - payload = { - "addons": { - "contact": {} - }, - "phone_number": self.phone_number_test - } - - response = client.phoneid(**payload) - - self.assertEqual(response.headers.get('Content-Type'), content_type_expected, "Content-Type args do not match expected") - self.assertEqual(response.status_code, status_code_expected, "Status code args do not match expected") - def test_phoneid_pid(self): client = PhoneIdClient(self.customer_id, self.api_key) diff --git a/tests/test_rest.py b/tests/test_rest.py index 9a6fd0b..128a62c 100644 --- a/tests/test_rest.py +++ b/tests/test_rest.py @@ -18,6 +18,7 @@ class TestRest(TestCase): def setUp(self): self.customer_id = "FFFFFFFF-EEEE-DDDD-1234-AB1234567890" self.api_key = "EXAMPLE----TE8sTgg45yusumoN6BYsBVkh+yRJ5czgsnCehZaOYldPJdmFh6NeX8kunZ2zU1YWaUw/0wV6xfw==" + self.rest_endpoint = "https://rest-api.telesign.com" def test_rest_client_constructor_basic(self): @@ -43,6 +44,19 @@ def test_rest_client_response_constructor_basic(self): self.assertEqual(response.ok, requests_response.ok) self.assertEqual(response.json, requests_response.json()) + def test_rest_client_response_constructor_from_full_service(self): + + client = RestClient(self.customer_id, + self.api_key, + self.rest_endpoint, + "python_telesign_enterprise", + "1.0.0", + "2.0.0") + + self.assertIn("OriginatingSDK/python_telesign_enterprise", client.user_agent) + self.assertIn("SDKVersion/1.0.0", client.user_agent) + self.assertIn("DependencySDKVersion/2.0.0", client.user_agent) + def test_generate_telesign_headers_with_post(self): method_name = 'POST' date_rfc2616 = 'Wed, 14 Dec 2016 18:20:12 GMT' @@ -276,4 +290,23 @@ def test_post_basic_auth(self, mock_generate_telesign_headers): self.assertEqual(post_args, expected_post_args, "client.session.post.call_args args do not match expected") self.assertEqual(post_kwargs, expected_post_kwargs, - "client.session.post.call_args kwargs do not match expected") \ No newline at end of file + "client.session.post.call_args kwargs do not match expected") + + def test_session_adapter_is_httpadapter(self): + client = RestClient(self.customer_id, self.api_key) + https_adapter = client.session.adapters["https://"] + import requests + self.assertIsInstance(https_adapter, requests.adapters.HTTPAdapter) + + @patch("time.time") + def test_session_refresh_on_pool_recycle(self, mock_time): + # Simulate time to force session recycling + mock_time.return_value = 1000 + client = RestClient(self.customer_id, self.api_key, pool_recycle=10) + created_at_first = client._session_created_at + # Advance time beyond the threshold + mock_time.return_value = 1012 + # Force a request (any method calls _ensure_session) + client._ensure_session() + created_at_second = client._session_created_at + self.assertNotEqual(created_at_first, created_at_second) \ No newline at end of file