From 16c42fb3685d8ef8cfc83a850aa839febe1eed30 Mon Sep 17 00:00:00 2001 From: milorenzo <59612324+miguel-lorenzo@users.noreply.github.com> Date: Thu, 16 Feb 2023 19:47:58 +0100 Subject: [PATCH] feat: support setting a timeout to all requests from Config object (#57) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: support setting a timeout to all requests from Config object * fix: accept/reject user methods were not returning a Result object * fix: use Field constraints to avoid mypy errors See: https://github.com/pydantic/pydantic/discussions/4162 --------- Co-authored-by: Fran García Salomón --- alice/auth/auth.py | 6 +- alice/auth/auth_client.py | 43 +- alice/config.py | 3 + alice/onboarding/onboarding.py | 8 +- alice/onboarding/onboarding_client.py | 564 ++++++++++++++++++-------- alice/onboarding/onboarding_errors.py | 6 + tests/test_integration_auth.py | 8 + tests/test_integration_onboarding.py | 23 ++ 8 files changed, 475 insertions(+), 186 deletions(-) diff --git a/alice/auth/auth.py b/alice/auth/auth.py index 1b1fc0f..2f5787e 100644 --- a/alice/auth/auth.py +++ b/alice/auth/auth.py @@ -24,6 +24,7 @@ def from_config(config: Config) -> "Auth": api_key=config.api_key, # type: ignore session=session, url=config.onboarding_url, + timeout=config.timeout, verbose=config.verbose, ) @@ -32,9 +33,12 @@ def __init__( api_key: str, session: Session, url: str = DEFAULT_URL, + timeout: Union[float, None] = None, verbose: Optional[bool] = False, ): - self._auth_client = AuthClient(url=url, api_key=api_key, session=session) + self._auth_client = AuthClient( + url=url, api_key=api_key, session=session, timeout=timeout + ) self.url = url self.verbose = verbose diff --git a/alice/auth/auth_client.py b/alice/auth/auth_client.py index f07999f..d55ed1d 100644 --- a/alice/auth/auth_client.py +++ b/alice/auth/auth_client.py @@ -1,19 +1,28 @@ import json import time from typing import Optional, Union +from unittest.mock import Mock import jwt +import requests from requests import Response, Session from alice.onboarding.tools import print_intro, print_response, timeit class AuthClient: - def __init__(self, url: str, api_key: str, session: Session): + def __init__( + self, + url: str, + api_key: str, + session: Session, + timeout: Union[float, None] = None, + ): self.url = url self._api_key = api_key self._login_token: Union[str, None] = None self.session = session + self.timeout = timeout @timeit def create_backend_token( @@ -35,7 +44,16 @@ def create_backend_token( final_url += f"/{user_id}" headers = {"Authorization": f"Bearer {self._login_token}"} - response = self.session.get(final_url, headers=headers) + try: + response = self.session.get( + final_url, headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + response = Mock(spec=Response) + response.json.return_value = {"message": "Request timed out"} + response.text.return_value = "Request timed out" + response.status_code = 408 + print_response(response=response, verbose=verbose) return response @@ -56,7 +74,15 @@ def create_user_token( final_url = f"{self.url}/user_token/{user_id}" headers = {"Authorization": f"Bearer {self._login_token}"} - response = self.session.get(final_url, headers=headers) + try: + response = self.session.get( + final_url, headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + response = Mock(spec=Response) + response.json.return_value = {"message": "Request timed out"} + response.text.return_value = "Request timed out" + response.status_code = 408 print_response(response=response, verbose=verbose) @@ -65,7 +91,16 @@ def create_user_token( def _create_login_token(self) -> Response: final_url = f"{self.url}/login_token" headers = {"apikey": self._api_key} - response = self.session.get(final_url, headers=headers) + try: + response = self.session.get( + final_url, headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + response = Mock(spec=Response) + response.json.return_value = {"message": "Request timed out"} + response.text.return_value = "Request timed out" + response.status_code = 408 + return response @staticmethod diff --git a/alice/config.py b/alice/config.py index 757cfa5..4e845aa 100644 --- a/alice/config.py +++ b/alice/config.py @@ -15,6 +15,9 @@ class Config: ) api_key: Union[str, None] = Field(default=None) sandbox_token: Union[str, None] = Field(default=None) + timeout: Union[float, None] = Field( + default=None, description="Timeout for every request in seconds", ge=1, le=100 + ) send_agent: bool = Field(default=True) verbose: bool = Field(default=False) session: Union[Session, None] = None diff --git a/alice/onboarding/onboarding.py b/alice/onboarding/onboarding.py index 84aa65b..df297c4 100644 --- a/alice/onboarding/onboarding.py +++ b/alice/onboarding/onboarding.py @@ -34,6 +34,7 @@ def from_config(config: Config) -> "Onboarding": return Onboarding( auth=Auth.from_config(config), url=config.onboarding_url, + timeout=config.timeout, send_agent=config.send_agent, verbose=config.verbose, session=session, @@ -44,11 +45,12 @@ def __init__( auth: Auth, session: Session, url: str = DEFAULT_URL, + timeout: Union[float, None] = None, send_agent: bool = True, verbose: bool = False, ): self.onboarding_client = OnboardingClient( - auth=auth, url=url, send_agent=send_agent, session=session + auth=auth, url=url, timeout=timeout, send_agent=send_agent, session=session ) self.url = url self.verbose = verbose @@ -1673,7 +1675,7 @@ def accept_user( verbose = self.verbose or verbose response = self.onboarding_client.accept_user( user_id=user_id, operator=operator, verbose=verbose - ) + ).unwrap_or_return() if response.status_code == 200: return isSuccess @@ -1714,7 +1716,7 @@ def reject_user( rejection_reasons=rejection_reasons, operator=operator, verbose=verbose, - ) + ).unwrap_or_return() if response.status_code == 200: return isSuccess diff --git a/alice/onboarding/onboarding_client.py b/alice/onboarding/onboarding_client.py index ab64ea4..0145685 100644 --- a/alice/onboarding/onboarding_client.py +++ b/alice/onboarding/onboarding_client.py @@ -4,12 +4,11 @@ from typing import Any, Dict, List, Optional, Union import requests -from meiga import Error, Result, Success, early_return +from meiga import Error, Failure, Result, Success, early_return from requests import Response, Session import alice from alice.auth.auth import Auth -from alice.auth.auth_errors import AuthError from alice.onboarding.enums.certificate_locale import CertificateLocale from alice.onboarding.enums.decision import Decision from alice.onboarding.enums.document_side import DocumentSide @@ -20,6 +19,7 @@ from alice.onboarding.models.bounding_box import BoundingBox from alice.onboarding.models.device_info import DeviceInfo from alice.onboarding.models.user_info import UserInfo +from alice.onboarding.onboarding_errors import OnboardingError from alice.onboarding.tools import print_intro, print_response, print_token, timeit DEFAULT_URL = "https://apis.alicebiometrics.com/onboarding" @@ -31,10 +31,12 @@ def __init__( auth: Auth, session: Session, url: str = DEFAULT_URL, + timeout: Union[float, None] = None, send_agent: bool = True, ): self.auth = auth self.url = url + self.timeout = timeout self.send_agent = send_agent self.session = session @@ -66,8 +68,10 @@ def healthcheck(self, verbose: Optional[bool] = False) -> Result[Response, Error """ print_intro("healthcheck", verbose=verbose) - response = self.session.get(f"{self.url}/healthcheck") - + try: + response = self.session.get(f"{self.url}/healthcheck", timeout=self.timeout) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="healthcheck")) print_response(response=response, verbose=verbose) return Success(response) @@ -79,7 +83,7 @@ def create_user( user_info: Union[UserInfo, None] = None, device_info: Union[DeviceInfo, None] = None, verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ It creates a new User in the onboarding service. @@ -115,9 +119,12 @@ def create_user( if device_info: data = data if data is not None else {} data.update(device_info.dict()) - - response = self.session.post(f"{self.url}/user", headers=headers, data=data) - + try: + response = self.session.post( + f"{self.url}/user", headers=headers, data=data, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="create_user")) print_response(response=response, verbose=verbose) return Success(response) @@ -126,7 +133,7 @@ def create_user( @timeit def delete_user( self, user_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Delete all the information of a user @@ -149,8 +156,13 @@ def delete_user( print_token("backend_token_with_user", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.delete(f"{self.url}/user", headers=headers) + try: + response = self.session.delete( + f"{self.url}/user", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="delete_user")) print_response(response=response, verbose=verbose) return Success(response) @@ -159,7 +171,7 @@ def delete_user( @timeit def get_user_status( self, user_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns User status to be used as feedback from the onboarding process @@ -183,8 +195,12 @@ def get_user_status( headers = self._auth_headers(user_token) - response = self.session.get(f"{self.url}/user/status", headers=headers) - + try: + response = self.session.get( + f"{self.url}/user/status", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_user_status")) print_response(response=response, verbose=verbose) return Success(response) @@ -193,7 +209,7 @@ def get_user_status( @timeit def get_users_stats( self, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns statistics about users in the Onboarding platform. @@ -214,15 +230,19 @@ def get_users_stats( print_token("backend_token", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.get(f"{self.url}/users/stats", headers=headers) - + try: + response = self.session.get( + f"{self.url}/users/stats", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_users_stats")) print_response(response=response, verbose=verbose) return Success(response) @early_return @timeit - def get_users(self, verbose: Optional[bool] = False) -> Result[Response, AuthError]: + def get_users(self, verbose: Optional[bool] = False) -> Result[Response, Error]: """ Returns all users you have created, sorted by creation date in descending order. @@ -243,7 +263,12 @@ def get_users(self, verbose: Optional[bool] = False) -> Result[Response, AuthErr print_token("backend_token", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.get(f"{self.url}/users", headers=headers) + try: + response = self.session.get( + f"{self.url}/users", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_users")) print_response(response=response, verbose=verbose) @@ -261,7 +286,7 @@ def get_users_status( sort_by: Union[str, None] = None, filter_field: Union[str, None] = None, filter_value: Union[str, None] = None, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns every UserStatus available for all the Users in the onboarding platform ordered by creation date. @@ -305,10 +330,14 @@ def get_users_status( if sort_by: url_query_params = url_query_params + f"&sort_by={sort_by}" - response = self.session.get( - f"{self.url}/users/status{url_query_params}", headers=headers - ) - + try: + response = self.session.get( + f"{self.url}/users/status{url_query_params}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_users_status")) print_response(response=response, verbose=verbose) return Success(response) @@ -323,7 +352,7 @@ def add_user_feedback( decision: Decision, additional_feedback: List[str] = [], verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Adds client's feedback about an user onboarding. Usually it comes after human review. @@ -362,9 +391,15 @@ def add_user_feedback( "additional_feedback": additional_feedback, } - response = self.session.post( - self.url + "/user/feedback", data=data, headers=headers - ) + try: + response = self.session.post( + self.url + "/user/feedback", + data=data, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="add_user_feedback")) print_response(response=response, verbose=verbose) @@ -374,7 +409,7 @@ def add_user_feedback( @timeit def add_selfie( self, user_id: str, media_data: bytes, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to upload for the first time the video of the user's face to the onboarding service. @@ -403,9 +438,15 @@ def add_selfie( files = {"video": ("video", media_data)} - response = self.session.post( - f"{self.url}/user/selfie", files=files, headers=headers - ) + try: + response = self.session.post( + f"{self.url}/user/selfie", + files=files, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="add_selfie")) print_response(response=response, verbose=verbose) @@ -418,7 +459,7 @@ def delete_selfie( user_id: str, selfie_id: Optional[str] = None, verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to delete the video of the user's face to the onboarding service. @@ -444,12 +485,19 @@ def delete_selfie( print_token("backend_token_with_user", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - if not selfie_id: - response = self.session.delete(f"{self.url}/user/selfie", headers=headers) - else: - response = self.session.delete( - f"{self.url}/user/selfie/{selfie_id}", headers=headers - ) + try: + if not selfie_id: + response = self.session.delete( + f"{self.url}/user/selfie", headers=headers, timeout=self.timeout + ) + else: + response = self.session.delete( + f"{self.url}/user/selfie/{selfie_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="delete_selfie")) print_response(response=response, verbose=verbose) @@ -459,7 +507,7 @@ def delete_selfie( @timeit def void_selfie( self, user_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to void the video of the user's face to the onboarding service. @@ -484,7 +532,12 @@ def void_selfie( headers = self._auth_headers(backend_token) - response = self.session.patch(f"{self.url}/user/selfie", headers=headers) + try: + response = self.session.patch( + f"{self.url}/user/selfie", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="void_selfie")) print_response(response=response, verbose=verbose) @@ -494,7 +547,7 @@ def void_selfie( @timeit def supported_documents( self, user_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This method is used to obtain a hierarchical-ordered dict with the information of the documents supported by the API. @@ -521,7 +574,12 @@ def supported_documents( headers = self._auth_headers(token) - response = self.session.get(f"{self.url}/documents/supported", headers=headers) + try: + response = self.session.get( + f"{self.url}/documents/supported", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="supported_documents")) print_response(response=response, verbose=verbose) return Success(response) @@ -534,7 +592,7 @@ def create_document( type: DocumentType, issuing_country: str, verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to obtain a new document_id @@ -563,9 +621,15 @@ def create_document( headers = self._auth_headers(user_token) data = {"type": type.value, "issuing_country": issuing_country} - response = self.session.post( - f"{self.url}/user/document", data=data, headers=headers - ) + try: + response = self.session.post( + f"{self.url}/user/document", + data=data, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="create_document")) print_response(response=response, verbose=verbose) @@ -575,7 +639,7 @@ def create_document( @timeit def delete_document( self, user_id: str, document_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Delete all the stored/extracted information from a document @@ -600,10 +664,14 @@ def delete_document( print_token("backend_token_with_user", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.delete( - f"{self.url}/user/document/{document_id}", headers=headers - ) - + try: + response = self.session.delete( + f"{self.url}/user/document/{document_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="delete_document")) print_response(response=response, verbose=verbose) return Success(response) @@ -612,7 +680,7 @@ def delete_document( @timeit def void_document( self, user_id: str, document_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Mark a document as invalid. @@ -637,9 +705,14 @@ def void_document( print_token("backend_token_with_user", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.patch( - f"{self.url}/user/document/{document_id}", headers=headers - ) + try: + response = self.session.patch( + f"{self.url}/user/document/{document_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="void_document")) print_response(response=response, verbose=verbose) @@ -658,7 +731,7 @@ def add_document( bounding_box: Union[BoundingBox, None] = None, fields: Union[Dict[str, Any], None] = None, verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to upload for the first time the photo or video of a document to the onboarding service. @@ -709,9 +782,16 @@ def add_document( files = {"image": ("image", media_data)} - response = self.session.put( - f"{self.url}/user/document", files=files, data=data, headers=headers - ) + try: + response = self.session.put( + f"{self.url}/user/document", + files=files, + data=data, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="add_document")) print_response(response=response, verbose=verbose) @@ -725,7 +805,7 @@ def document_properties( type: DocumentType, issuing_country: str, verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns the properties of a previously added document, such as face, MRZ or NFC availability @@ -755,10 +835,15 @@ def document_properties( headers = self._auth_headers(user_token) data = {"type": type.value, "issuing_country": issuing_country} - response = self.session.post( - f"{self.url}/user/document/properties", data=data, headers=headers - ) - + try: + response = self.session.post( + f"{self.url}/user/document/properties", + data=data, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="document_properties")) print_response(response=response, verbose=verbose) return Success(response) @@ -767,7 +852,7 @@ def document_properties( @timeit def delete_other_trusted_document( self, user_id: str, document_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Delete all the stored/extracted information from an other trusted document @@ -792,9 +877,16 @@ def delete_other_trusted_document( print_token("backend_token_with_user", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.delete( - f"{self.url}/user/other-trusted-document/{document_id}", headers=headers - ) + try: + response = self.session.delete( + f"{self.url}/user/other-trusted-document/{document_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure( + OnboardingError.timeout(operation="delete_other_trusted_document") + ) print_response(response=response, verbose=verbose) @@ -804,7 +896,7 @@ def delete_other_trusted_document( @timeit def void_other_trusted_document( self, user_id: str, document_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Mark other trusted document as invalid. @@ -829,10 +921,16 @@ def void_other_trusted_document( print_token("backend_token_with_user", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.patch( - f"{self.url}/user/other-trusted-document/{document_id}", headers=headers - ) - + try: + response = self.session.patch( + f"{self.url}/user/other-trusted-document/{document_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure( + OnboardingError.timeout(operation="void_other_trusted_document") + ) print_response(response=response, verbose=verbose) return Success(response) @@ -845,7 +943,7 @@ def add_other_trusted_document( media_data: bytes, category: Optional[str] = None, verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to upload an other trusted document (OTD) to the onboarding service. @@ -878,12 +976,18 @@ def add_other_trusted_document( data = dict(category=category) if category else dict() - response = self.session.post( - f"{self.url}/user/other-trusted-document", - files=files, - data=data, - headers=headers, - ) + try: + response = self.session.post( + f"{self.url}/user/other-trusted-document", + files=files, + data=data, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure( + OnboardingError.timeout(operation="add_other_trusted_document") + ) print_response(response=response, verbose=verbose) @@ -895,7 +999,7 @@ def create_report( user_id: str, version: Version = Version.DEFAULT, verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to get the report of the onboarding process for a specific user. @@ -926,7 +1030,12 @@ def create_report( headers = self._auth_headers(backend_user_token) headers["Alice-Report-Version"] = version.value - response = self.session.get(f"{self.url}/user/report", headers=headers) + try: + response = self.session.get( + f"{self.url}/user/report", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="create_report")) print_response(response=response, verbose=verbose) @@ -940,7 +1049,7 @@ def create_certificate( locale: CertificateLocale = CertificateLocale.EN, template_name: str = "default", verbose: Optional[bool] = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to create a Certificate (Signed PDF Report) of the onboarding process for a specific user. It returns a identifier (certificate_id) as a reference of created resource. @@ -973,9 +1082,16 @@ def create_certificate( options = {"template_name": template_name, "locale": locale.value} headers = self._auth_headers(backend_user_token) headers["Content-Type"] = "application/json" - response = self.session.post( - f"{self.url}/user/certificate", data=json.dumps(options), headers=headers - ) + try: + response = self.session.post( + f"{self.url}/user/certificate", + data=json.dumps(options), + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="create_certificate")) + print_response(response=response, verbose=verbose) return Success(response) @@ -984,7 +1100,7 @@ def create_certificate( @timeit def retrieve_certificate( self, user_id: str, certificate_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to retrieve a Certificate (Signed PDF Report) of the onboarding process for a specific user. @@ -1012,9 +1128,14 @@ def retrieve_certificate( ).unwrap_or_return() print_token("backend_token_with_user", backend_user_token, verbose=verbose) headers = self._auth_headers(backend_user_token) - response = self.session.get( - f"{self.url}/user/certificate/{certificate_id}", headers=headers - ) + try: + response = self.session.get( + f"{self.url}/user/certificate/{certificate_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="retrieve_certificate")) print_response(response=response, verbose=verbose) @@ -1024,7 +1145,7 @@ def retrieve_certificate( @timeit def retrieve_certificates( self, user_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns summary info for created certificates @@ -1047,8 +1168,12 @@ def retrieve_certificates( ).unwrap_or_return() print_token("backend_token_with_user", backend_user_token, verbose=verbose) headers = self._auth_headers(backend_user_token) - response = self.session.get(f"{self.url}/user/certificates", headers=headers) - + try: + response = self.session.get( + f"{self.url}/user/certificates", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="retrieve_certificates")) print_response(response=response, verbose=verbose) return Success(response) @@ -1057,7 +1182,7 @@ def retrieve_certificates( @timeit def screening( self, user_id: str, detail: bool = False, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is used to check on the user using different databases & lists (sanctions, PEP, etc).. It returns retrieved information from public lists. @@ -1088,8 +1213,10 @@ def screening( if detail: url += "/detail" - response = self.session.get(url, headers=headers) - + try: + response = self.session.get(url, headers=headers, timeout=self.timeout) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="screening")) print_response(response=response, verbose=verbose) return Success(response) @@ -1098,7 +1225,7 @@ def screening( @timeit def screening_monitor_add( self, user_id: str, verbose: Optional[bool] = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is adds a user to the AML monitoring list. @@ -1120,12 +1247,21 @@ def screening_monitor_add( backend_user_token = self.auth.create_backend_token( user_id=user_id ).unwrap_or_return() - print_token("backend_token_with_user", backend_user_token, verbose=verbose) + print_token( + "backend_token_with_user", + backend_user_token, + verbose=verbose, + ) headers = self._auth_headers(backend_user_token) - response = self.session.get( - f"{self.url}/user/screening/monitor", headers=headers - ) + try: + response = self.session.get( + f"{self.url}/user/screening/monitor", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="screening_monitor_add")) print_response(response=response, verbose=verbose) @@ -1135,7 +1271,7 @@ def screening_monitor_add( @timeit def screening_monitor_delete( self, user_id: str, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ This call is adds a user to the AML monitoring list. @@ -1160,9 +1296,16 @@ def screening_monitor_delete( print_token("backend_token_with_user", backend_user_token, verbose=verbose) headers = self._auth_headers(backend_user_token) - response = self.session.delete( - f"{self.url}/user/screening/monitor", headers=headers - ) + try: + response = self.session.delete( + f"{self.url}/user/screening/monitor", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure( + OnboardingError.timeout(operation="screening_monitor_delete") + ) print_response(response=response, verbose=verbose) @@ -1172,7 +1315,7 @@ def screening_monitor_delete( @timeit def screening_monitor_open_alerts( self, start_index: int = 0, size: int = 100, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Retrieves from the monitoring list the users with open alerts @@ -1196,11 +1339,16 @@ def screening_monitor_open_alerts( print_token("backend_token", backend_token, verbose=verbose) headers = self._auth_headers(backend_token) - response = self.session.get( - f"{self.url}/users/screening/monitor/alerts?start_index={start_index}&size={size}", - headers=headers, - ) - + try: + response = self.session.get( + f"{self.url}/users/screening/monitor/alerts?start_index={start_index}&size={size}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure( + OnboardingError.timeout(operation="screening_monitor_open_alerts") + ) print_response(response=response, verbose=verbose) return Success(response) @@ -1213,7 +1361,7 @@ def identify_user( probe_user_ids: List[str], version: Version = Version.DEFAULT, verbose: bool = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Identifies (1:N matching) a user against a N-length list of users. @@ -1245,10 +1393,15 @@ def identify_user( data = {"user_ids": probe_user_ids} - response = self.session.post( - f"{self.url}/user/identify", headers=headers, data=data - ) - + try: + response = self.session.post( + f"{self.url}/user/identify", + headers=headers, + data=data, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="identify_user")) print_response(response=response, verbose=verbose) return Success(response) @@ -1257,7 +1410,7 @@ def identify_user( @timeit def authorize_user( self, user_id: str, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Authorizes a user. Now it can be authenticate. @@ -1281,8 +1434,12 @@ def authorize_user( print_token("backend_token_with_user", backend_user_token, verbose=verbose) headers = self._auth_headers(backend_user_token) - response = self.session.post(f"{self.url}/user/authorize", headers=headers) - + try: + response = self.session.post( + f"{self.url}/user/authorize", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="authorize_user")) print_response(response=response, verbose=verbose) return Success(response) @@ -1291,7 +1448,7 @@ def authorize_user( @timeit def deauthorize_user( self, user_id: str, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Deauthorizes a user. Now it cannot be authenticate. @@ -1315,8 +1472,12 @@ def deauthorize_user( print_token("backend_token_with_user", backend_user_token, verbose=verbose) headers = self._auth_headers(backend_user_token) - response = self.session.post(f"{self.url}/user/deauthorize", headers=headers) - + try: + response = self.session.post( + f"{self.url}/user/deauthorize", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="deauthorize_user")) print_response(response=response, verbose=verbose) return Success(response) @@ -1325,7 +1486,7 @@ def deauthorize_user( @timeit def authenticate_user( self, user_id: str, media_data: bytes, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Authenticates a previously registered User against a given media to verify the identity @@ -1353,10 +1514,15 @@ def authenticate_user( files = {"video": ("video", media_data)} - response = self.session.post( - f"{self.url}/user/authenticate", files=files, headers=headers - ) - + try: + response = self.session.post( + f"{self.url}/user/authenticate", + files=files, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="authenticate_user")) print_response(response=response, verbose=verbose) return Success(response) @@ -1365,7 +1531,7 @@ def authenticate_user( @timeit def get_authentications_ids( self, user_id: str, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns all authentication you have performed for a User, sorted by creation date in descending order. @@ -1391,10 +1557,14 @@ def get_authentications_ids( headers = self._auth_headers(backend_user_token) - response = self.session.get( - f"{self.url}/user/authentications/ids", headers=headers - ) - + try: + response = self.session.get( + f"{self.url}/user/authentications/ids", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_authentications_ids")) print_response(response=response, verbose=verbose) return Success(response) @@ -1409,7 +1579,7 @@ def get_authentications( descending: bool = True, version: Version = Version.DEFAULT, verbose: bool = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns all authentications you have performed for a User. @@ -1448,10 +1618,14 @@ def get_authentications( f"?page={str(page)}&page_size={str(page_size)}&descending={str(descending)}" ) - response = self.session.get( - f"{self.url}/user/authentications" + url_query_params, headers=headers - ) - + try: + response = self.session.get( + f"{self.url}/user/authentications" + url_query_params, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_authentications")) print_response(response=response, verbose=verbose) return Success(response) @@ -1464,7 +1638,7 @@ def get_authentication( authentication_id: str, version: Version = Version.DEFAULT, verbose: bool = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Gets a information from a previous authentication using the verification_id @@ -1495,10 +1669,14 @@ def get_authentication( headers = self._auth_headers(backend_user_token) headers["Alice-Authentication-Version"] = version.value - response = self.session.get( - f"{self.url}/user/authentication/{authentication_id}", headers=headers - ) - + try: + response = self.session.get( + f"{self.url}/user/authentication/{authentication_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_authentication")) print_response(response=response, verbose=verbose) return Success(response) @@ -1507,7 +1685,7 @@ def get_authentication( @timeit def retrieve_media( self, user_id: str, media_id: str, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns the binary data of a media resource @@ -1535,10 +1713,14 @@ def retrieve_media( headers = self._auth_headers(backend_user_token) - response = self.session.get( - f"{self.url}/media/{media_id}/download", headers=headers - ) - + try: + response = self.session.get( + f"{self.url}/media/{media_id}/download", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="retrieve_media")) print_response(response=response, verbose=verbose) return Success(response) @@ -1547,7 +1729,7 @@ def retrieve_media( @timeit def download( self, user_id: str, href: str, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns the binary data of a media resource @@ -1575,8 +1757,10 @@ def download( headers = self._auth_headers(backend_user_token) - response = self.session.get(href, headers=headers) - + try: + response = self.session.get(href, headers=headers, timeout=self.timeout) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="download")) print_response(response=response, verbose=verbose) return Success(response) @@ -1589,7 +1773,7 @@ def request_duplicates_search( end_date: datetime, resource_type: DuplicatesResourceType, verbose: bool = False, - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Returns the binary data of a media resource @@ -1622,9 +1806,17 @@ def request_duplicates_search( "end_date": end_date.isoformat(), "resource_type": resource_type.value, } - response = requests.post( - f"{self.url}/duplicates/search", data=data, headers=headers - ) + try: + response = requests.post( + f"{self.url}/duplicates/search", + data=data, + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure( + OnboardingError.timeout(operation="request_duplicates_search") + ) print_response(response=response, verbose=verbose) return Success(response) @@ -1633,7 +1825,7 @@ def request_duplicates_search( @timeit def get_duplicates_search( self, search_id: str, verbose: bool = False - ) -> Result[Response, AuthError]: + ) -> Result[Response, Error]: """ Retrieves a previously requested duplicates search from the onboarding platform @@ -1658,18 +1850,21 @@ def get_duplicates_search( headers = self._auth_headers(backend_token) - response = requests.get( - f"{self.url}/duplicates/search/{search_id}", headers=headers - ) + try: + response = requests.get( + f"{self.url}/duplicates/search/{search_id}", + headers=headers, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_duplicates_search")) print_response(response=response, verbose=verbose) return Success(response) @early_return @timeit - def get_duplicates_searches( - self, verbose: bool = False - ) -> Result[Response, AuthError]: + def get_duplicates_searches(self, verbose: bool = False) -> Result[Response, Error]: """ Retrieves all previously requested duplicates searches from the onboarding platform @@ -1692,7 +1887,12 @@ def get_duplicates_searches( headers = self._auth_headers(backend_token) - response = requests.get(f"{self.url}/duplicates/searches", headers=headers) + try: + response = requests.get( + f"{self.url}/duplicates/searches", headers=headers, timeout=self.timeout + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="get_duplicates_searches")) print_response(response=response, verbose=verbose) return Success(response) @@ -1700,7 +1900,7 @@ def get_duplicates_searches( @timeit def accept_user( self, user_id: str, operator: str = "auto", verbose: bool = False - ) -> Response: + ) -> Result[Response, Error]: """ Mark a user state as ACCEPTED @@ -1726,17 +1926,21 @@ def accept_user( print_token("backend_token_with_user", backend_user_token, verbose=verbose) headers = self._auth_headers(backend_user_token) - response = requests.post( - f"{self.url}/user/state/accept", - headers=headers, - json={ - "operator": operator, - }, - ) + try: + response = requests.post( + f"{self.url}/user/state/accept", + headers=headers, + json={ + "operator": operator, + }, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="accept_user")) print_response(response=response, verbose=verbose) - return response + return Success(response) @timeit def reject_user( @@ -1745,7 +1949,7 @@ def reject_user( rejection_reasons: Optional[List[Dict[str, str]]] = None, operator: str = "auto", verbose: bool = False, - ) -> Response: + ) -> Result[Response, Error]: """ Mark a user state as REJECTED @@ -1773,12 +1977,16 @@ def reject_user( print_token("backend_token_with_user", backend_user_token, verbose=verbose) headers = self._auth_headers(backend_user_token) - response = requests.post( - f"{self.url}/user/state/reject", - headers=headers, - json={"operator": operator, "rejection_reasons": rejection_reasons}, - ) + try: + response = requests.post( + f"{self.url}/user/state/reject", + headers=headers, + json={"operator": operator, "rejection_reasons": rejection_reasons}, + timeout=self.timeout, + ) + except requests.exceptions.Timeout: + return Failure(OnboardingError.timeout(operation="accept_user")) print_response(response=response, verbose=verbose) - return response + return Success(response) diff --git a/alice/onboarding/onboarding_errors.py b/alice/onboarding/onboarding_errors.py index 7686458..deae9fb 100644 --- a/alice/onboarding/onboarding_errors.py +++ b/alice/onboarding/onboarding_errors.py @@ -32,3 +32,9 @@ def from_response(operation: str, response: Response) -> "OnboardingError": except Exception: message = {"message": "no content"} return OnboardingError(operation=operation, code=code, message=message) + + @staticmethod + def timeout(operation: str) -> "OnboardingError": + return OnboardingError( + operation=operation, code=408, message={"message": "Request timed out"} + ) diff --git a/tests/test_integration_auth.py b/tests/test_integration_auth.py index 1349a35..c7c793a 100644 --- a/tests/test_integration_auth.py +++ b/tests/test_integration_auth.py @@ -31,6 +31,14 @@ def should_create_a_valid_backend_token(self, given_valid_api_key): result = auth.create_backend_token() result.assert_success(value_is_instance_of=str) + def should_create_a_valid_backend_token_with_timeout(self, given_valid_api_key): + + config = Config(api_key=given_valid_api_key, timeout=5) + auth = Auth.from_config(config) + + result = auth.create_backend_token() + result.assert_success(value_is_instance_of=str) + def should_create_a_valid_backend_token_with_user(self, given_valid_api_key): config = Config(api_key=given_valid_api_key) diff --git a/tests/test_integration_onboarding.py b/tests/test_integration_onboarding.py index d52c8be..da89b18 100644 --- a/tests/test_integration_onboarding.py +++ b/tests/test_integration_onboarding.py @@ -7,6 +7,7 @@ from alice.onboarding.enums.document_type import DocumentType from alice.onboarding.enums.version import Version from alice.onboarding.models.report.report import Report +from alice.onboarding.onboarding_errors import OnboardingError @pytest.mark.unit @@ -20,6 +21,28 @@ def test_should_return_an_error_when_the_api_key_is_not_configured(): result.assert_failure() +@pytest.mark.unit +def test_should_timeout_when_time_exceeded( + given_valid_api_key, given_any_selfie_image_media_data +): + config = Config(api_key=given_valid_api_key, timeout=1) + onboarding = Onboarding.from_config(config) + + user_id = onboarding.create_user( + user_info=UserInfo(first_name="Alice", last_name="Biometrics"), + device_info=DeviceInfo(device_platform="Android"), + ).unwrap_or_throw() + + result = onboarding.add_selfie( + user_id=user_id, media_data=given_any_selfie_image_media_data + ) + result.assert_failure( + value_is_equal_to=OnboardingError( + operation="add_selfie", code=408, message={"message": "Request timed out"} + ) + ) + + @pytest.mark.unit def test_should_do_complete_onboarding_process( given_valid_api_key,