diff --git a/.coveragerc b/.coveragerc index 51873f2a..f5c96c80 100644 --- a/.coveragerc +++ b/.coveragerc @@ -2,4 +2,8 @@ omit = yoti_python_sdk/tests/** yoti_python_sdk/protobuf/**/* - examples/** \ No newline at end of file + examples/** + +[report] +exclude_lines = + raise NotImplementedError \ No newline at end of file diff --git a/requirements.in b/requirements.in index 5b435b12..6c920272 100644 --- a/requirements.in +++ b/requirements.in @@ -12,3 +12,4 @@ requests>=2.20.0 urllib3>=1.24.2 deprecated==1.2.6 wheel==0.24.0 +iso8601==0.1.12 diff --git a/requirements.txt b/requirements.txt index 6125c9a6..be3f3714 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,6 +12,7 @@ cryptography==2.8 # via -r requirements.in, pyopenssl deprecated==1.2.6 # via -r requirements.in future==0.18.2 # via -r requirements.in idna==2.7 # via requests +iso8601==0.1.12 # via -r requirements.in itsdangerous==0.24 # via -r requirements.in pbr==1.10.0 # via -r requirements.in protobuf==3.11.3 # via -r requirements.in diff --git a/setup.py b/setup.py index f1bde78f..e6148705 100644 --- a/setup.py +++ b/setup.py @@ -31,6 +31,7 @@ "future>=0.11.0", "asn1==2.2.0", "pyopenssl>=18.0.0", + "iso8601==0.1.12", ], extras_require={ "examples": [ diff --git a/sonar-project.properties b/sonar-project.properties index c505ef56..f5b692d9 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -2,7 +2,7 @@ sonar.host.url = https://sonarcloud.io sonar.organization = getyoti sonar.projectKey = getyoti:python sonar.projectName = Python SDK -sonar.projectVersion = 2.10.2 +sonar.projectVersion = 2.11.0 sonar.exclusions = yoti_python_sdk/tests/**,examples/**,yoti_python_sdk/protobuf/**/* sonar.python.pylint.reportPath = coverage.out diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py index 8486f9a9..a33747d5 100644 --- a/yoti_python_sdk/__init__.py +++ b/yoti_python_sdk/__init__.py @@ -9,7 +9,7 @@ "YOTI_API_URL": "https://api.yoti.com", "YOTI_API_PORT": 443, "YOTI_API_VERSION": "v1", - "YOTI_API_VERIFY_SSL": "true" + "YOTI_API_VERIFY_SSL": "true", } main_ns = {} @@ -23,19 +23,29 @@ __version__ = main_ns["__version__"] YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"]) + +YOTI_PROFILE_ENDPOINT = "/api/v1" +YOTI_DOC_SCAN_ENDPOINT = "/idverify/v1" + YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"]) YOTI_API_VERSION = environ.get("YOTI_API_VERSION", DEFAULTS["YOTI_API_VERSION"]) -YOTI_API_ENDPOINT = environ.get("YOTI_API_ENDPOINT", "{0}:{1}/api/{2}".format( - YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION -)) -YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]) +# Fully formatted API URLs +YOTI_API_ENDPOINT = environ.get( + "YOTI_API_ENDPOINT", + "{0}:{1}{2}".format(YOTI_API_URL, YOTI_API_PORT, YOTI_PROFILE_ENDPOINT), +) +YOTI_DOC_SCAN_API_URL = environ.get( + "YOTI_DOC_SCAN_API_URL", + "{0}:{1}{2}".format(YOTI_API_URL, YOTI_API_PORT, YOTI_DOC_SCAN_ENDPOINT), +) + +YOTI_API_VERIFY_SSL = environ.get( + "YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"] +) if YOTI_API_VERIFY_SSL.lower() == "false": YOTI_API_VERIFY_SSL = False else: YOTI_API_VERIFY_SSL = True -__all__ = [ - "Client", - __version__ -] +__all__ = ["Client", __version__] diff --git a/yoti_python_sdk/doc_scan/__init__.py b/yoti_python_sdk/doc_scan/__init__.py new file mode 100644 index 00000000..c4abde59 --- /dev/null +++ b/yoti_python_sdk/doc_scan/__init__.py @@ -0,0 +1,21 @@ +from .session.create.check.document_authenticity import ( + RequestedDocumentAuthenticityCheckBuilder, +) +from .session.create.check.face_match import RequestedFaceMatchCheckBuilder +from .session.create.check.liveness import RequestedLivenessCheckBuilder +from .session.create.task.text_extraction import RequestedTextExtractionTaskBuilder +from .session.create.notification_config import NotificationConfigBuilder +from .session.create.sdk_config import SdkConfigBuilder +from .session.create.session_spec import SessionSpecBuilder +from .client import DocScanClient + +__all__ = [ + RequestedDocumentAuthenticityCheckBuilder, + RequestedLivenessCheckBuilder, + RequestedFaceMatchCheckBuilder, + RequestedTextExtractionTaskBuilder, + SessionSpecBuilder, + NotificationConfigBuilder, + SdkConfigBuilder, + DocScanClient, +] diff --git a/yoti_python_sdk/doc_scan/client.py b/yoti_python_sdk/doc_scan/client.py new file mode 100644 index 00000000..cee7300a --- /dev/null +++ b/yoti_python_sdk/doc_scan/client.py @@ -0,0 +1,171 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import json + +import yoti_python_sdk +from yoti_python_sdk.doc_scan.endpoint import Endpoint +from yoti_python_sdk.doc_scan.session.retrieve.create_session_result import ( + CreateSessionResult, +) +from yoti_python_sdk.doc_scan.session.retrieve.get_session_result import ( + GetSessionResult, +) +from yoti_python_sdk.doc_scan.session.retrieve.media_value import MediaValue +from yoti_python_sdk.http import SignedRequest +from yoti_python_sdk.utils import YotiEncoder +from .exception import DocScanException + + +class DocScanClient(object): + """ + Client used for communication with the Yoti Doc Scan service where any + signed request is required + """ + + def __init__(self, sdk_id, key, api_url=None): + self.__sdk_id = sdk_id + self.__key = key + if api_url is not None: + self.__api_url = api_url + else: + self.__api_url = yoti_python_sdk.YOTI_DOC_SCAN_ENDPOINT + + def create_session(self, session_spec): + """ + Creates a Doc Scan session using the supplied session specification + + :param session_spec: the session specification + :type session_spec: SessionSpec + :return: the create session result + :rtype: CreateSessionResult + :raises DocScanException: if there was an error creating the session + """ + payload = json.dumps(session_spec, cls=YotiEncoder).encode("utf-8") + + request = ( + SignedRequest.builder() + .with_post() + .with_pem_file(self.__key) + .with_base_url(self.__api_url) + .with_endpoint(Endpoint.create_docs_session_path()) + .with_param("sdkId", self.__sdk_id) + .with_payload(payload) + .with_header("Content-Type", "application/json") + .build() + ) + response = request.execute() + + if response.status_code != 201: + raise DocScanException("Failed to create session", response) + + data = json.loads(response.text) + return CreateSessionResult(data) + + def get_session(self, session_id): + """ + Retrieves the state of a previously created Yoti Doc Scan session + + :param session_id: the session ID + :type session_id: str + :return: the session state + :rtype: GetSessionResult + :raises DocScanException: if there was an error retrieving the session + """ + request = ( + SignedRequest.builder() + .with_get() + .with_pem_file(self.__key) + .with_base_url(self.__api_url) + .with_endpoint(Endpoint.retrieve_docs_session_path(session_id)) + .with_param("sdkId", self.__sdk_id) + .build() + ) + response = request.execute() + + if response.status_code != 200: + raise DocScanException("Failed to retrieve session", response) + + data = json.loads(response.text) + return GetSessionResult(data) + + def delete_session(self, session_id): + """ + Deletes a previously created Yoti Doc Scan session and + all of its related resources + + :param session_id: the session id to delete + :type session_id: str + :rtype: None + :raises DocScanException: if there was an error deleting the session + """ + request = ( + SignedRequest.builder() + .with_http_method("DELETE") + .with_pem_file(self.__key) + .with_base_url(self.__api_url) + .with_endpoint(Endpoint.delete_docs_session_path(session_id)) + .with_param("sdkId", self.__sdk_id) + .build() + ) + response = request.execute() + + if response.status_code < 200 or response.status_code >= 300: + raise DocScanException("Failed to delete session", response) + + def get_media_content(self, session_id, media_id): + """ + Retrieves media related to a Yoti Doc Scan session + based on the supplied media ID + + :param session_id: the session ID + :type session_id: str + :param media_id: the media ID + :type media_id: str + :return: the media + :rtype: MediaValue + :raises DocScanException: if there was an error retrieving the media content + """ + request = ( + SignedRequest.builder() + .with_get() + .with_pem_file(self.__key) + .with_base_url(self.__api_url) + .with_endpoint(Endpoint.get_media_content_path(session_id, media_id)) + .with_param("sdkId", self.__sdk_id) + .build() + ) + response = request.execute() + + if response.status_code != 200: + raise DocScanException("Failed to retrieve media content", response) + + media_mime_type = response.headers["Content-Type"] + media_content = response.content + return MediaValue(media_mime_type, media_content) + + def delete_media_content(self, session_id, media_id): + """ + Deletes media related to a Yoti Doc Scan session + based on the supplied media ID + + :param session_id: the session ID + :type session_id: str + :param media_id: the media ID + :type media_id: str + :rtype: None + :raises DocScanException: if there was an error deleting the media content + """ + request = ( + SignedRequest.builder() + .with_http_method("DELETE") + .with_pem_file(self.__key) + .with_base_url(self.__api_url) + .with_endpoint(Endpoint.delete_media_path(session_id, media_id)) + .with_param("sdkId", self.__sdk_id) + .build() + ) + + response = request.execute() + if response.status_code < 200 or response.status_code >= 300: + raise DocScanException("Failed to delete media content", response) diff --git a/yoti_python_sdk/doc_scan/constants.py b/yoti_python_sdk/doc_scan/constants.py new file mode 100644 index 00000000..3af3a8c3 --- /dev/null +++ b/yoti_python_sdk/doc_scan/constants.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +ID_DOCUMENT_AUTHENTICITY = "ID_DOCUMENT_AUTHENTICITY" +ID_DOCUMENT_TEXT_DATA_CHECK = "ID_DOCUMENT_TEXT_DATA_CHECK" +ID_DOCUMENT_TEXT_DATA_EXTRACTION = "ID_DOCUMENT_TEXT_DATA_EXTRACTION" +ID_DOCUMENT_FACE_MATCH = "ID_DOCUMENT_FACE_MATCH" +LIVENESS = "LIVENESS" +ZOOM = "ZOOM" + +CAMERA = "CAMERA" +CAMERA_AND_UPLOAD = "CAMERA_AND_UPLOAD" + +RESOURCE_UPDATE = "RESOURCE_UPDATE" +TASK_COMPLETION = "TASK_COMPLETION" +CHECK_COMPLETION = "CHECK_COMPLETION" +SESSION_COMPLETION = "SESSION_COMPLETION" + +ALWAYS = "ALWAYS" +FALLBACK = "FALLBACK" +NEVER = "NEVER" diff --git a/yoti_python_sdk/doc_scan/endpoint.py b/yoti_python_sdk/doc_scan/endpoint.py new file mode 100644 index 00000000..74425fd6 --- /dev/null +++ b/yoti_python_sdk/doc_scan/endpoint.py @@ -0,0 +1,22 @@ +class Endpoint(object): + @staticmethod + def create_docs_session_path(): + return "/sessions" + + @staticmethod + def retrieve_docs_session_path(session_id): + return "/sessions/{sessionId}".format(sessionId=session_id) + + @staticmethod + def delete_docs_session_path(session_id): + return Endpoint.retrieve_docs_session_path(session_id) + + @staticmethod + def get_media_content_path(session_id, media_id): + return "/sessions/{sessionId}/media/{mediaId}/content".format( + sessionId=session_id, mediaId=media_id + ) + + @staticmethod + def delete_media_path(session_id, media_id): + return Endpoint.get_media_content_path(session_id, media_id) diff --git a/yoti_python_sdk/doc_scan/exception/__init__.py b/yoti_python_sdk/doc_scan/exception/__init__.py new file mode 100644 index 00000000..aab9483e --- /dev/null +++ b/yoti_python_sdk/doc_scan/exception/__init__.py @@ -0,0 +1,3 @@ +from .doc_scan_exception import DocScanException + +__all__ = [DocScanException] diff --git a/yoti_python_sdk/doc_scan/exception/doc_scan_exception.py b/yoti_python_sdk/doc_scan/exception/doc_scan_exception.py new file mode 100644 index 00000000..efe0f323 --- /dev/null +++ b/yoti_python_sdk/doc_scan/exception/doc_scan_exception.py @@ -0,0 +1,60 @@ +class DocScanException(Exception): + """ + Exception thrown by the Yoti Doc Scan client + when an error has occurred when communicating with the API + """ + + def __init__(self, message, response): + """ + :param message: the exception message + :type message: str + :param response: the http response + :type response: requests.Response + """ + Exception.__init__(self) + + self.__message = message + self.__response = response + + @property + def message(self): + """ + Get the specific exception message + + :return: the exception message + :rtype: str + """ + return self.__message + + @property + def status_code(self): + """ + Get the status code of the HTTP response + + :return: the status code + :rtype: int or None + """ + return self.__response.status_code + + @property + def text(self): + """ + Return the HTTP response body as text + + :return: the body as text + :rtype: str + """ + return self.__response.text + + @property + def content(self): + """ + Return the HTTP response body as bytes + + :return: the body as bytes + :rtype: bytearray or None + """ + return self.__response.content + + def __str__(self): + return self.__message diff --git a/yoti_python_sdk/doc_scan/session/__init__.py b/yoti_python_sdk/doc_scan/session/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/doc_scan/session/create/__init__.py b/yoti_python_sdk/doc_scan/session/create/__init__.py new file mode 100644 index 00000000..7b7896e6 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/__init__.py @@ -0,0 +1,15 @@ +from .check.document_authenticity import RequestedDocumentAuthenticityCheckBuilder +from .check.face_match import RequestedFaceMatchCheckBuilder +from .check.liveness import RequestedLivenessCheckBuilder +from .notification_config import NotificationConfigBuilder +from .session_spec import SessionSpecBuilder +from .sdk_config import SdkConfigBuilder + +__all__ = [ + RequestedDocumentAuthenticityCheckBuilder, + RequestedFaceMatchCheckBuilder, + RequestedLivenessCheckBuilder, + NotificationConfigBuilder, + SessionSpecBuilder, + SdkConfigBuilder, +] diff --git a/yoti_python_sdk/doc_scan/session/create/check/__init__.py b/yoti_python_sdk/doc_scan/session/create/check/__init__.py new file mode 100644 index 00000000..3f14e174 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/check/__init__.py @@ -0,0 +1,9 @@ +from .document_authenticity import RequestedDocumentAuthenticityCheckBuilder +from .face_match import RequestedFaceMatchCheckBuilder +from .liveness import RequestedLivenessCheckBuilder + +__all__ = [ + RequestedDocumentAuthenticityCheckBuilder, + RequestedFaceMatchCheckBuilder, + RequestedLivenessCheckBuilder, +] diff --git a/yoti_python_sdk/doc_scan/session/create/check/document_authenticity.py b/yoti_python_sdk/doc_scan/session/create/check/document_authenticity.py new file mode 100644 index 00000000..032e0a8f --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/check/document_authenticity.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.constants import ID_DOCUMENT_AUTHENTICITY +from yoti_python_sdk.utils import YotiSerializable +from .requested_check import RequestedCheck + + +class RequestedDocumentAuthenticityCheckConfig(YotiSerializable): + """ + The configuration applied when creating a Document Authenticity Check + """ + + def to_json(self): + return {} + + +class RequestedDocumentAuthenticityCheck(RequestedCheck): + """ + Requests creation of a Document Authenticity Check + """ + + def __init__(self, config): + """ + :param config: the requested document authenticity check configuration + :type config: RequestedDocumentAuthenticityCheckConfig + """ + self.__config = config + + @property + def type(self): + return ID_DOCUMENT_AUTHENTICITY + + @property + def config(self): + return self.__config + + +class RequestedDocumentAuthenticityCheckBuilder(object): + """ + Builder to assist creation of :class:`RequestedDocumentAuthenticityCheck` + """ + + @staticmethod + def build(): + config = RequestedDocumentAuthenticityCheckConfig() + return RequestedDocumentAuthenticityCheck(config) diff --git a/yoti_python_sdk/doc_scan/session/create/check/face_match.py b/yoti_python_sdk/doc_scan/session/create/check/face_match.py new file mode 100644 index 00000000..8ae2807f --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/check/face_match.py @@ -0,0 +1,97 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.utils import YotiSerializable +from .requested_check import RequestedCheck + + +class RequestedFaceMatchCheckConfig(YotiSerializable): + """ + The configuration applied when creating a FaceMatch Check + """ + + def __init__(self, manual_check): + """ + :param manual_check: the manual check value + :type manual_check: str + """ + self.__manual_check = manual_check + + @property + def manual_check(self): + """ + Returns a value for a manual check for a given + FaceMatch Check + + :return: the manual check value + :rtype: str + """ + return self.__manual_check + + def to_json(self): + return {"manual_check": self.manual_check} + + +class RequestedFaceMatchCheck(RequestedCheck): + """ + Requests creation of a FaceMatch Check + """ + + def __init__(self, config): + """ + :param config: the requested FaceMatch check configuration + :type config: RequestedFaceMatchCheckConfig + """ + self.__config = config + + @property + def type(self): + return constants.ID_DOCUMENT_FACE_MATCH + + @property + def config(self): + return self.__config + + +class RequestedFaceMatchCheckBuilder(object): + """ + Builder to assist with creation of :class:`RequestedFaceMatchCheck` + """ + + def __init__(self): + self.__manual_check = None + + def with_manual_check_always(self): + """ + Sets the value of manual check to "ALWAYS" + + :return: the builder + :rtype: RequestedFaceMatchCheckBuilder + """ + self.__manual_check = constants.ALWAYS + return self + + def with_manual_check_fallback(self): + """ + Sets the value of manual check to "FALLBACK" + + :return: the builder + :rtype: RequestedFaceMatchCheckBuilder + """ + self.__manual_check = constants.FALLBACK + return self + + def with_manual_check_never(self): + """ + Sets the value of manual check to "NEVER" + + :return: the builder + :rtype: RequestedFaceMatchCheckBuilder + """ + self.__manual_check = constants.NEVER + return self + + def build(self): + config = RequestedFaceMatchCheckConfig(self.__manual_check) + return RequestedFaceMatchCheck(config) diff --git a/yoti_python_sdk/doc_scan/session/create/check/liveness.py b/yoti_python_sdk/doc_scan/session/create/check/liveness.py new file mode 100644 index 00000000..82cc2fe3 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/check/liveness.py @@ -0,0 +1,112 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.utils import YotiSerializable +from .requested_check import RequestedCheck + + +class RequestedLivenessCheckConfig(YotiSerializable): + """ + The configuration applied when creating a Liveness Check + """ + + def __init__(self, liveness_type, max_retries): + """ + :param liveness_type: the liveness type + :type liveness_type: str + :param max_retries: the maximum number of retries + :type max_retries: int + """ + self.__liveness_type = liveness_type + self.__max_retries = max_retries + + @property + def liveness_type(self): + """ + The type of the liveness check, e.g. "ZOOM" + + :return: the liveness type + """ + return self.__liveness_type + + @property + def max_retries(self): + """ + The maximum number of retries a user is allowed for a liveness check + + :return: the maximum number of retries + """ + return self.__max_retries + + def to_json(self): + return {"liveness_type": self.liveness_type, "max_retries": self.max_retries} + + +class RequestedLivenessCheck(RequestedCheck): + """ + Requests creation of a Liveness Check + """ + + def __init__(self, liveness_check_config): + """ + :param liveness_check_config: the requested liveness check configuration + :type liveness_check_config: RequestedLivenessCheckConfig + """ + self.__config = liveness_check_config + + @property + def type(self): + return constants.LIVENESS + + @property + def config(self): + return self.__config + + +class RequestedLivenessCheckBuilder(object): + """ + Builder to assist creation of :class:`RequestedLivenessCheck` + """ + + def __init__(self): + self.__liveness_type = None + self.__max_retries = None + + def for_zoom_liveness(self): + """ + Sets the liveness type to "ZOOM" + + :return: the builder + :rtype: RequestedLivenessCheckBuilder + """ + return self.with_liveness_type(constants.ZOOM) + + def with_liveness_type(self, liveness_type): + """ + Sets the liveness type on the builder + + :param liveness_type: the liveness type + :type liveness_type: str + :return: the builder + :rtype: RequestedLivenessCheckBuilder + """ + self.__liveness_type = liveness_type + return self + + def with_max_retries(self, max_retries): + """ + Sets the maximum number of retries allowed for liveness check + on the builder + + :param max_retries: the maximum number of retries + :type max_retries: int + :return: the builder + :rtype: RequestedLivenessCheckBuilder + """ + self.__max_retries = max_retries + return self + + def build(self): + config = RequestedLivenessCheckConfig(self.__liveness_type, self.__max_retries) + return RequestedLivenessCheck(config) diff --git a/yoti_python_sdk/doc_scan/session/create/check/requested_check.py b/yoti_python_sdk/doc_scan/session/create/check/requested_check.py new file mode 100644 index 00000000..71a293e0 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/check/requested_check.py @@ -0,0 +1,36 @@ +from abc import ABCMeta +from abc import abstractmethod + +from yoti_python_sdk.utils import YotiSerializable + + +class RequestedCheck(YotiSerializable): + """ + Requests creation of a Check to be performed on a document + """ + + __metaclass__ = ABCMeta + + @property + @abstractmethod + def type(self): + """ + Return the type of the Check to create + + :return: the type + :rtype: str + """ + raise NotImplementedError + + @property + @abstractmethod + def config(self): + """ + Return configuration to apply to the Check + + :return: the configuration + """ + raise NotImplementedError + + def to_json(self): + return {"type": self.type, "config": self.config} diff --git a/yoti_python_sdk/doc_scan/session/create/notification_config.py b/yoti_python_sdk/doc_scan/session/create/notification_config.py new file mode 100644 index 00000000..8df275a4 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/notification_config.py @@ -0,0 +1,162 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.constants import CHECK_COMPLETION +from yoti_python_sdk.doc_scan.constants import RESOURCE_UPDATE +from yoti_python_sdk.doc_scan.constants import SESSION_COMPLETION +from yoti_python_sdk.doc_scan.constants import TASK_COMPLETION +from yoti_python_sdk.utils import YotiSerializable + + +class NotificationConfig(YotiSerializable): + """ + Configures call-back Notifications to some backend endpoint provided by the Relying Business. + + Notifications can be configured to notify a clients backend of certain events, avoiding the need + to poll for the state of the Session. + """ + + def __init__(self, auth_token, endpoint, topics=None): + """ + :param auth_token: the authorization token + :type auth_token: str + :param endpoint: the endpoint + :type endpoint: str + :param topics: the list of topics + :type topics: list[str] + """ + if topics is None: + topics = [] + + self.__auth_token = auth_token + self.__endpoint = endpoint + self.__topics = list(set(topics)) # Get unique values + + @property + def auth_token(self): + """ + The authorization token to be included in call-back messages + + :return: the authorization token + :rtype: str + """ + return self.__auth_token + + @property + def endpoint(self): + """ + The endpoint that notifications should be sent to + + :return: the endpoint + :rtype: str + """ + return self.__endpoint + + @property + def topics(self): + """ + The list of topics that should trigger notifications + + :return: the list of topics + :rtype: list[str] + """ + return self.__topics + + def to_json(self): + return { + "auth_token": self.auth_token, + "endpoint": self.endpoint, + "topics": self.topics, + } + + +class NotificationConfigBuilder(object): + """ + Builder to assist in the creation of :class:`NotificationConfig` + """ + + def __init__(self): + self.__auth_token = None + self.__endpoint = None + self.__topics = [] + + def with_auth_token(self, token): + """ + Sets the authorization token to be included in call-back messages + + :param token: the authorization token + :type token: str + :return: the builder + :rtype: NotificationConfigBuilder + """ + self.__auth_token = token + return self + + def with_endpoint(self, endpoint): + """ + Sets the endpoint that notifications should be sent to + + :param endpoint: the endpoint + :type endpoint: str + :return: the builder + :rtype: NotificationConfigBuilder + """ + self.__endpoint = endpoint + return self + + def with_topic(self, topic): + """ + Adds a topic to the list of topics that trigger notification messages + + :param topic: the topic + :type topic: str + :return: the builder + :rtype: NotificationConfigBuilder + """ + self.__topics.append(topic) + return self + + def for_resource_update(self): + """ + Adds RESOURCE_UPDATE to the list of topics that trigger notification messages + + :return: the builder + :rtype: NotificationConfigBuilder + """ + return self.with_topic(RESOURCE_UPDATE) + + def for_task_completion(self): + """ + Adds TASK_COMPLETION to the list of topics that trigger notification messages + + :return: the builder + :rtype: NotificationConfigBuilder + """ + return self.with_topic(TASK_COMPLETION) + + def for_session_completion(self): + """ + Adds SESSION_COMPLETION to the list of topics that trigger notification messages + + :return: the builder + :rtype: NotificationConfigBuilder + """ + return self.with_topic(SESSION_COMPLETION) + + def for_check_completion(self): + """ + Adds CHECK_COMPLETION to the list of topics that trigger notification messages + + :return: the builder + :rtype: NotificationConfigBuilder + """ + return self.with_topic(CHECK_COMPLETION) + + def build(self): + """ + Builds the :class:`NotificationConfig` using the supplied values + + :return: the build notification config + :rtype: NotificationConfig + """ + return NotificationConfig(self.__auth_token, self.__endpoint, self.__topics) diff --git a/yoti_python_sdk/doc_scan/session/create/sdk_config.py b/yoti_python_sdk/doc_scan/session/create/sdk_config.py new file mode 100644 index 00000000..a9246068 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/sdk_config.py @@ -0,0 +1,276 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.constants import CAMERA +from yoti_python_sdk.doc_scan.constants import CAMERA_AND_UPLOAD +from yoti_python_sdk.utils import YotiSerializable + + +class SdkConfig(YotiSerializable): + """ + Provides configuration properties using by the web/native clients + """ + + def __init__( + self, + allowed_capture_methods, + primary_colour, + secondary_colour, + font_colour, + locale, + preset_issuing_country, + success_url, + error_url, + ): + """ + :param allowed_capture_methods: the allowed capture methods + :type allowed_capture_methods: str + :param primary_colour: the primary colour + :type primary_colour: str + :param secondary_colour: the secondary colour + :type secondary_colour: str + :param font_colour: the font colour + :type font_colour: str + :param locale: the locale + :type locale: str + :param preset_issuing_country: the preset issuing country + :type preset_issuing_country: str + :param success_url: the success url + :type success_url: str + :param error_url: the error url + :type error_url: str + """ + self.__allowed_capture_methods = allowed_capture_methods + self.__primary_colour = primary_colour + self.__secondary_colour = secondary_colour + self.__font_colour = font_colour + self.__locale = locale + self.__preset_issuing_country = preset_issuing_country + self.__success_url = success_url + self.__error_url = error_url + + @property + def allowed_capture_methods(self): + """ + The methods allowed for capturing document images + + :return: the allowed capture methods + """ + return self.__allowed_capture_methods + + @property + def primary_colour(self): + """ + The primary colour + + :return: the primary colour + """ + return self.__primary_colour + + @property + def secondary_colour(self): + """ + The secondary colour + + :return: the secondary colour + """ + return self.__secondary_colour + + @property + def font_colour(self): + """ + The font colour + + :return: the font colour + """ + return self.__font_colour + + @property + def locale(self): + """ + The locale + + :return: the locale + """ + return self.__locale + + @property + def preset_issuing_country(self): + """ + The preset issuing country + + :return: the preset issuing country + """ + return self.__preset_issuing_country + + @property + def success_url(self): + """ + The success URL + + :return: the success url + """ + return self.__success_url + + @property + def error_url(self): + """ + The error URL + + :return: the error url + """ + return self.__error_url + + def to_json(self): + return { + "allowed_capture_methods": self.allowed_capture_methods, + "primary_colour": self.primary_colour, + "secondary_colour": self.secondary_colour, + "font_colour": self.font_colour, + "locale": self.locale, + "preset_issuing_country": self.preset_issuing_country, + "success_url": self.success_url, + "error_url": self.error_url, + } + + +class SdkConfigBuilder(object): + """ + Builder to assist in the creation of :class:`SdkConfig` + """ + + def __init__(self): + self.__allowed_capture_methods = None + self.__primary_colour = None + self.__secondary_colour = None + self.__font_colour = None + self.__locale = None + self.__preset_issuing_country = None + self.__success_url = None + self.__error_url = None + + def with_allowed_capture_methods(self, allowed_capture_methods): + """ + Sets the allowed capture methods on the builder + + :param allowed_capture_methods: the allowed capture methods + :type allowed_capture_methods: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__allowed_capture_methods = allowed_capture_methods + return self + + def with_allows_camera(self): + """ + Sets the allowed capture method to "CAMERA" + + :return: the builder + :rtype: SdkConfigBuilder + """ + return self.with_allowed_capture_methods(CAMERA) + + def with_allows_camera_and_upload(self): + """ + Sets the allowed capture method to "CAMERA_AND_UPLOAD" + + :return: the builder + :rtype: SdkConfigBuilder + """ + return self.with_allowed_capture_methods(CAMERA_AND_UPLOAD) + + def with_primary_colour(self, colour): + """ + Sets the primary colour to be used by the web/native client + + :param colour: the primary colour, hexadecimal value e.g. #ff0000 + :type colour: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__primary_colour = colour + return self + + def with_secondary_colour(self, colour): + """ + Sets the secondary colour to be used by the web/native client (used on the button) + + :param colour: the secondary colour, hexadecimal value e.g. #ff0000 + :type colour: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__secondary_colour = colour + return self + + def with_font_colour(self, colour): + """ + Sets the font colour to be used by the web/native client (used on the button) + + :param colour: the font colour, hexadecimal value e.g. #ff0000 + :type colour: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__font_colour = colour + return self + + def with_locale(self, locale): + """ + Sets the language locale use by the web/native client + + :param locale: the locale, e.g. "en" + :type locale: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__locale = locale + return self + + def with_preset_issuing_country(self, country): + """ + Sets the preset issuing country used by the web/native client + + :param country: the preset issuing country + :type country: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__preset_issuing_country = country + return self + + def with_success_url(self, url): + """ + Sets the success URL for the redirect that follows the web/native client uploading documents successfully + + :param url: the success URL + :type url: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__success_url = url + return self + + def with_error_url(self, url): + """ + Sets the error URL for the redirect that follows the web/native client uploading documents unsuccessfully + + :param url: the error URL + :type url: str + :return: the builder + :rtype: SdkConfigBuilder + """ + self.__error_url = url + return self + + def build(self): + return SdkConfig( + self.__allowed_capture_methods, + self.__primary_colour, + self.__secondary_colour, + self.__font_colour, + self.__locale, + self.__preset_issuing_country, + self.__success_url, + self.__error_url, + ) diff --git a/yoti_python_sdk/doc_scan/session/create/session_spec.py b/yoti_python_sdk/doc_scan/session/create/session_spec.py new file mode 100644 index 00000000..eb7aa90d --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/session_spec.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.utils import YotiSerializable + + +class SessionSpec(YotiSerializable): + """ + Definition for the Doc Scan Session to be created + """ + + def __init__( + self, + client_session_token_ttl, + resources_ttl, + user_tracking_id, + notifications, + sdk_config, + requested_checks=None, + requested_tasks=None, + ): + """ + :param client_session_token_ttl: the client session token TTL + :type client_session_token_ttl: int + :param resources_ttl: the resources TTL + :type resources_ttl: int + :param user_tracking_id: the user tracking ID + :type user_tracking_id: str + :param notifications: the notification configuration + :type notifications: NotificationConfig + :param sdk_config: the SDK configuration + :type sdk_config: SdkConfig + :param requested_checks: the list of requested checks + :type requested_checks: list[RequestedCheck] + :param requested_tasks: the list of requested tasks + :type requested_tasks: list[RequestedTask] + """ + if requested_tasks is None: + requested_tasks = [] + if requested_checks is None: + requested_checks = [] + + self.__client_session_token_ttl = client_session_token_ttl + self.__resources_ttl = resources_ttl + self.__user_tracking_id = user_tracking_id + self.__notifications = notifications + self.__sdk_config = sdk_config + self.__requested_checks = requested_checks + self.__requested_tasks = requested_tasks + + @property + def client_session_token_ttl(self): + """ + Client-session-token time-to-live to apply to the created Session + + :return: the client-session-token time-to-live + :rtype: int + """ + return self.__client_session_token_ttl + + @property + def resources_ttl(self): + """ + Time-to-live used for all Resources created in the course of the session + + :return: the time-to-live for Resources + :rtype: int + """ + return self.__resources_ttl + + @property + def user_tracking_id(self): + """ + User tracking ID, for the Relying Business to track returning users + + :return: the user tracking ID + :rtype: str + """ + return self.__user_tracking_id + + @property + def notifications(self): + """ + :class:`NotificationConfig` for configuring call-back messages + + :return: the notification config + :rtype: NotificationConfig + """ + return self.__notifications + + @property + def sdk_config(self): + """ + Retrieves the SDK configuration set of the session specification + + :return: the SDK config + :rtype: SdkConfig + """ + return self.__sdk_config + + @property + def requested_checks(self): + """ + List of :class:`RequestedCheck` objects defining the Checks to be performed + on each Document + + :return: the requested checks + :rtype: list[RequestedCheck] + """ + return self.__requested_checks + + @property + def requested_tasks(self): + """ + List of :class:`RequestedTask` objects defining the Tasks to be performed + on each Document + + :return: the requested tasks + :rtype: list[RequestedTask] + """ + return self.__requested_tasks + + def to_json(self): + return { + "client_session_token_ttl": self.client_session_token_ttl, + "resources_ttl": self.resources_ttl, + "user_tracking_id": self.user_tracking_id, + "notifications": self.notifications, + "requested_checks": self.requested_checks, + "requested_tasks": self.requested_tasks, + "sdk_config": self.sdk_config, + } + + +class SessionSpecBuilder(object): + """ + Builder to assist the creation of :class:`SessionSpec` + """ + + def __init__(self): + self.__client_session_token_ttl = None + self.__resources_ttl = None + self.__user_tracking_id = None + self.__notifications = None + self.__sdk_config = None + self.__requested_checks = [] + self.__requested_tasks = [] + + def with_client_session_token_ttl(self, value): + """ + Sets the client session token TTL (time-to-live) + + :param value: the client session token TTL + :type value: int + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__client_session_token_ttl = value + return self + + def with_resources_ttl(self, value): + """ + Sets the resources TTL (time-to-live) + + :param value: the resources TTL + :type value: int + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__resources_ttl = value + return self + + def with_user_tracking_id(self, value): + """ + Sets the user tracking ID + + :param value: the user tracking ID + :type value: str + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__user_tracking_id = value + return self + + def with_notifications(self, notifications): + """ + Sets the notification configuration + + :param notifications: the notification config + :type notifications: NotificationConfig + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__notifications = notifications + return self + + def with_requested_check(self, check): + """ + Adds a :class:`RequestedCheck` to the list + + :param check: the check to add + :type check: RequestedCheck + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__requested_checks.append(check) + return self + + def with_requested_task(self, task): + """ + Adds a :class:`RequestedTask` to the list + + :param task: the task to add + :type task: RequestedTask + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__requested_tasks.append(task) + return self + + def with_sdk_config(self, value): + """ + Sets the SDK configuration + + :param value: the SDK config + :type value: SdkConfig + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__sdk_config = value + return self + + def build(self): + """ + Builds a :class:`SessionSpec` using the supplied values + + :return: the built Session Specification + :rtype: SessionSpec + """ + return SessionSpec( + self.__client_session_token_ttl, + self.__resources_ttl, + self.__user_tracking_id, + self.__notifications, + self.__sdk_config, + self.__requested_checks, + self.__requested_tasks, + ) diff --git a/yoti_python_sdk/doc_scan/session/create/task/__init__.py b/yoti_python_sdk/doc_scan/session/create/task/__init__.py new file mode 100644 index 00000000..44755304 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/task/__init__.py @@ -0,0 +1,3 @@ +from .text_extraction import RequestedTextExtractionTaskBuilder + +__all__ = [RequestedTextExtractionTaskBuilder] diff --git a/yoti_python_sdk/doc_scan/session/create/task/requested_task.py b/yoti_python_sdk/doc_scan/session/create/task/requested_task.py new file mode 100644 index 00000000..aa67c8e7 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/task/requested_task.py @@ -0,0 +1,36 @@ +from abc import ABCMeta +from abc import abstractmethod + +from yoti_python_sdk.utils import YotiSerializable + + +class RequestedTask(YotiSerializable): + """ + Requests creation of a Task to be performed on each document + """ + + __metaclass__ = ABCMeta + + @property + @abstractmethod + def type(self): + """ + Returns the type of the Task to create + + :return: the type + :rtype: str + """ + raise NotImplementedError + + @property + @abstractmethod + def config(self): + """ + Configuration to apply to the Task + + :return: the configuration + """ + raise NotImplementedError + + def to_json(self): + return {"type": self.type, "config": self.config} diff --git a/yoti_python_sdk/doc_scan/session/create/task/text_extraction.py b/yoti_python_sdk/doc_scan/session/create/task/text_extraction.py new file mode 100644 index 00000000..e15939cb --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/task/text_extraction.py @@ -0,0 +1,91 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.utils import YotiSerializable +from .requested_task import RequestedTask + + +class RequestedTextExtractionTaskConfig(YotiSerializable): + def __init__(self, manual_check): + """ + :param manual_check: the manual check value + :type manual_check: str + """ + self.__manual_check = manual_check + + @property + def manual_check(self): + """ + Describes the manual fallback behaviour applied to each Task + + :return: the manual check value + """ + return self.__manual_check + + def to_json(self): + return {"manual_check": self.manual_check} + + +class RequestedTextExtractionTask(RequestedTask): + """ + Builder to assist creation of :class:`RequestedTextExtractionTask` + """ + + def __init__(self, config): + """ + :param config: the text extraction task configuration + :type config: RequestedTextExtractionTaskConfig + """ + self.__config = config + + @property + def type(self): + return constants.ID_DOCUMENT_TEXT_DATA_EXTRACTION + + @property + def config(self): + return self.__config + + +class RequestedTextExtractionTaskBuilder(object): + """ + Builder to assist creation of :class:`RequestedTextExtractionTask` + """ + + def __init__(self): + self.__manual_check = None + + def with_manual_check_always(self): + """ + Sets the manual check value to be "ALWAYS" + + :return: the builder + :rtype: RequestedTextExtractionTaskBuilder + """ + self.__manual_check = constants.ALWAYS + return self + + def with_manual_check_fallback(self): + """ + Sets the manual check value to be "FALLBACK" + + :return: the builder + :rtype: RequestedTextExtractionTaskBuilder + """ + self.__manual_check = constants.FALLBACK + return self + + def with_manual_check_never(self): + """ + Sets the manual check value to be "NEVER" + + :return: the builder + :rtype: RequestedTextExtractionTaskBuilder + """ + self.__manual_check = constants.NEVER + return self + + def build(self): + config = RequestedTextExtractionTaskConfig(self.__manual_check) + return RequestedTextExtractionTask(config) diff --git a/yoti_python_sdk/doc_scan/session/retrieve/__init__.py b/yoti_python_sdk/doc_scan/session/retrieve/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/doc_scan/session/retrieve/breakdown_response.py b/yoti_python_sdk/doc_scan/session/retrieve/breakdown_response.py new file mode 100644 index 00000000..20622087 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/breakdown_response.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +class BreakdownResponse(object): + """ + Represents one breakdown item for a given check + """ + + def __init__(self, data): + """ + :param data: the data to parse + :type data: dict + """ + self.__sub_check = data.get("sub_check", None) + self.__result = data.get("result", None) + self.__details = [DetailsResponse(detail) for detail in data.get("details", [])] + + @property + def sub_check(self): + """ + The sub check value for the breakdown + + :return: the sub check value + :rtype: str or None + """ + return self.__sub_check + + @property + def result(self): + """ + The result of the sub check + + :return: the result + :rtype: str or None + """ + return self.__result + + @property + def details(self): + """ + The details of the sub check + + :return: the details + :rtype: list[DetailsResponse] + """ + return self.__details + + +class DetailsResponse(object): + """ + Represents a specific detail for a breakdown + """ + + def __init__(self, data): + self.__name = data.get("name", None) + self.__value = data.get("value", None) + + @property + def name(self): + """ + The name of the details item + + :return: the name + :rtype: str or None + """ + return self.__name + + @property + def value(self): + """ + The value of the details item + + :return: the value + :rtype: str or None + """ + return self.__value diff --git a/yoti_python_sdk/doc_scan/session/retrieve/check_response.py b/yoti_python_sdk/doc_scan/session/retrieve/check_response.py new file mode 100644 index 00000000..c5660ab0 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/check_response.py @@ -0,0 +1,170 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from iso8601 import ParseError +from iso8601 import iso8601 + +from .generated_media import GeneratedMedia +from .report_response import ReportResponse + + +class CheckResponse(object): + """ + Represents the base attributes for a check for any given session + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__state = data.get("state", None) + self.__type = data.get("type", None) + self.__resources_used = data.get("resources_used", []) + + self.__report = ( + ReportResponse(data["report"]) + if data.get("report", None) is not None + else None + ) + self.__generated_media = [ + GeneratedMedia(media) for media in data.get("generated_media", []) + ] + + self.__created = self.__parse_date(data.get("created", None)) + self.__last_updated = self.__parse_date(data.get("last_updated", None)) + + @staticmethod + def __parse_date(date): + """ + Attempts to parse a date from string using the + iso8601 library. Returns None if there was an error + + :param date: the datestring to parse + :type date: str + :return: the parsed date + :rtype: datetime.datetime or None + """ + if date is None: + return date + + try: + return iso8601.parse_date(date) + except ParseError: + return None + + @property + def id(self): + """ + The ID of the check + + :return: the ID + :rtype: str or None + """ + return self.__id + + @property + def type(self): + """ + The type of the check + + :return: the type + :rtype: str or None + """ + return self.__type + + @property + def state(self): + """ + The state of the check, e.g. "COMPLETED" + + :return: the state + :rtype: str or None + """ + return self.__state + + @property + def resources_used(self): + """ + The resources used by the check + + :return: the list of resources used + :rtype: list[str] + """ + return self.__resources_used + + @property + def created(self): + """ + The date the check was created + + :return: the created date + :rtype: datetime.datetime or None + """ + return self.__created + + @property + def last_updated(self): + """ + The date the check was last updated + + :return: the last updated date + :rtype: datetime.datetime or None + """ + return self.__last_updated + + @property + def generated_media(self): + """ + The list of media generated by the check + + :return: the list of generated media + :rtype: list[GeneratedMedia] + """ + return self.__generated_media + + @property + def report(self): + """ + Report for the check + + :return: the report + :rtype: ReportResponse or None + """ + return self.__report + + +class AuthenticityCheckResponse(CheckResponse): + """ + Represents a Document Authenticity check for a given session + """ + + pass + + +class FaceMatchCheckResponse(CheckResponse): + """ + Represents a FaceMatch Check for a given session + """ + + pass + + +class LivenessCheckResponse(CheckResponse): + """ + Represents a Liveness Check for a given session + """ + + pass + + +class TextDataCheckResponse(CheckResponse): + """ + Represents a Text Data check for a given session + """ + + pass diff --git a/yoti_python_sdk/doc_scan/session/retrieve/create_session_result.py b/yoti_python_sdk/doc_scan/session/retrieve/create_session_result.py new file mode 100644 index 00000000..c64dd9da --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/create_session_result.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +class CreateSessionResult(object): + """ + The response to a successful CreateSession call + """ + + def __init__(self, data=None): + """ + :param data: the data + :type data: dict or None + """ + if data is None: + data = dict() + + self.__client_session_token_ttl = data.get("client_session_token_ttl", None) + self.__session_id = data.get("session_id", None) + self.__client_session_token = data.get("client_session_token", None) + + @property + def client_session_token_ttl(self): + """ + Returns the time-to-live (TTL) for the client session + token for the created session + + :return: the client session token TTL + :rtype: int or None + """ + return self.__client_session_token_ttl + + @property + def client_session_token(self): + """ + Returns the client session token for the created session + + :return: the client session token + :rtype: str or None + """ + return self.__client_session_token + + @property + def session_id(self): + """ + Session ID of the created session + + :return: the session ID + :rtype: str or None + """ + return self.__session_id diff --git a/yoti_python_sdk/doc_scan/session/retrieve/document_fields_response.py b/yoti_python_sdk/doc_scan/session/retrieve/document_fields_response.py new file mode 100644 index 00000000..1d8b3621 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/document_fields_response.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class DocumentFieldsResponse(object): + """ + Represents the document fields response + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + if "media" in data.keys(): + self.__media = MediaResponse(data["media"]) + else: + self.__media = None + + @property + def media(self): + """ + The media object for the document fields + + :return: the media + :rtype: MediaResponse or None + """ + return self.__media diff --git a/yoti_python_sdk/doc_scan/session/retrieve/face_map_response.py b/yoti_python_sdk/doc_scan/session/retrieve/face_map_response.py new file mode 100644 index 00000000..881cd538 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/face_map_response.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class FaceMapResponse(object): + """ + Represents a FaceMap response object + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__media = MediaResponse(data["media"]) if "media" in data.keys() else None + + @property + def media(self): + """ + Returns the associated media of the FaceMap + + :return: the media + :rtype: MediaResponse or None + """ + return self.__media diff --git a/yoti_python_sdk/doc_scan/session/retrieve/frame_response.py b/yoti_python_sdk/doc_scan/session/retrieve/frame_response.py new file mode 100644 index 00000000..14e98726 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/frame_response.py @@ -0,0 +1,30 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class FrameResponse(object): + """ + Represents a frame of a resource + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__media = MediaResponse(data["media"]) if "media" in data.keys() else None + + @property + def media(self): + """ + Returns the media associated with the frame + + :return: the media + :rtype: MediaResponse or None + """ + return self.__media diff --git a/yoti_python_sdk/doc_scan/session/retrieve/generated_check_response.py b/yoti_python_sdk/doc_scan/session/retrieve/generated_check_response.py new file mode 100644 index 00000000..eddcb508 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/generated_check_response.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +class GeneratedCheckResponse(object): + """ + Represents a check response that has been generated by + the session + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__type = data.get("type", None) + + @property + def id(self): + """ + The id of the generated check + + :return: the id + :rtype: str or None + """ + return self.__id + + @property + def type(self): + """ + Returns the type of the generated check + + :return: the type + :rtype: str or None + """ + return self.__type + + +class GeneratedTextDataCheckResponse(GeneratedCheckResponse): + """ + Represents a generated Text Data check response + """ + + pass diff --git a/yoti_python_sdk/doc_scan/session/retrieve/generated_media.py b/yoti_python_sdk/doc_scan/session/retrieve/generated_media.py new file mode 100644 index 00000000..61dfae63 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/generated_media.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + + +class GeneratedMedia(object): + """ + Represents media that has been generated by the session + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__type = data.get("type", None) + + @property + def id(self): + """ + The ID of the generated media + + :return: the ID + :rtype: str or None + """ + return self.__id + + @property + def type(self): + """ + The type of the generated media, e.g. "JSON" + + :return: the type + :rtype: str or None + """ + return self.__type diff --git a/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py b/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py new file mode 100644 index 00000000..7405b3e5 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py @@ -0,0 +1,174 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan import constants +from .check_response import AuthenticityCheckResponse +from .check_response import CheckResponse +from .check_response import FaceMatchCheckResponse +from .check_response import LivenessCheckResponse +from .check_response import TextDataCheckResponse +from .resource_container import ResourceContainer + + +class GetSessionResult(object): + """ + Represents all information about the state of a session + at the time of the call, including check information, + resources etc. + """ + + def __init__(self, data): + """ + :param data: the data to parse + :type data: dict + """ + self.__client_session_token_ttl = data.get("client_session_token_ttl", None) + self.__session_id = data.get("session_id", None) + self.__user_tracking_id = data.get("user_tracking_id", None) + self.__state = data.get("state", None) + self.__client_session_token = data.get("client_session_token", None) + self.__checks = [self.__parse_check(check) for check in data.get("checks", [])] + + resources = data.get("resources", None) + self.__resources = ResourceContainer(resources) or None + + @staticmethod + def __parse_check(check): + """ + Parses a check into a sub-type of :class:`CheckResponse`, + or falls back to CheckResponse if an unknown type + + :param check: the check object + :type check: dict + :return: the parsed check + :rtype: CheckResponse + """ + types = { + constants.ID_DOCUMENT_AUTHENTICITY: AuthenticityCheckResponse, + constants.ID_DOCUMENT_FACE_MATCH: FaceMatchCheckResponse, + constants.ID_DOCUMENT_TEXT_DATA_CHECK: TextDataCheckResponse, + constants.LIVENESS: LivenessCheckResponse, + } + clazz = types.get(check.get("type", None), CheckResponse) + return clazz(check) + + @property + def client_session_token_ttl(self): + """ + The client session token time-to-live (TTL) + + :return: the client session token ttl + :rtype: int or None + """ + return self.__client_session_token_ttl + + @property + def session_id(self): + """ + The session ID + + :return: the session id + :rtype: str or None + """ + return self.__session_id + + @property + def user_tracking_id(self): + """ + The user tracking ID for the session + + :return: the user tracking id + :rtype: str or None + """ + return self.__user_tracking_id + + @property + def state(self): + """ + The state of the session, represented as a string e.g. "COMPLETED" + + :return: the state + :rtype: str or None + """ + return self.__state + + @property + def client_session_token(self): + """ + The client session token + + :return: the client session token + :rtype: str or None + """ + return self.__client_session_token + + @property + def checks(self): + """ + The list of Checks associated with the session + + :return: the list of Checks + :rtype: list[CheckResponse] + """ + return self.__checks + + def __checks_of_type(self, clazz): + """ + Filter the list of checks by the class type + + :param clazz: the class + :type clazz: tuple[type] + :return: + :rtype: + """ + return [check for check in self.checks if isinstance(check, clazz)] + + @property + def authenticity_checks(self): + """ + A filtered list of checks, returning only document authenticity checks + + :return: the document authenticity checks + :rtype: list[AuthenticityCheckResponse] + """ + return self.__checks_of_type((AuthenticityCheckResponse,)) + + @property + def face_match_checks(self): + """ + A filtered list of checks, returning only FaceMatch checks + + :return: the FaceMatch checks + :rtype: list[FaceMatchCheckResponse] + """ + return self.__checks_of_type((FaceMatchCheckResponse,)) + + @property + def text_data_checks(self): + """ + A filtered list of checks, returning only Text Data checks + + :return: the Text Data checks + :rtype: list[TextDataCheckResponse] + """ + return self.__checks_of_type((TextDataCheckResponse,)) + + @property + def liveness_checks(self): + """ + A filtered list of checks, returning only Liveness checks + + :return: the Liveness checks + :rtype: list[LivenessCheckResponse] + """ + return self.__checks_of_type((LivenessCheckResponse,)) + + @property + def resources(self): + """ + The resources associated with the session + + :return: the resources + :rtype: ResourceContainer or None + """ + return self.__resources diff --git a/yoti_python_sdk/doc_scan/session/retrieve/id_document_resource_response.py b/yoti_python_sdk/doc_scan/session/retrieve/id_document_resource_response.py new file mode 100644 index 00000000..79c31257 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/id_document_resource_response.py @@ -0,0 +1,93 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.session.retrieve.document_fields_response import ( + DocumentFieldsResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.page_response import PageResponse +from yoti_python_sdk.doc_scan.session.retrieve.task_response import TaskResponse + + +class IdDocumentResourceResponse(object): + """ + Represents an Identity Document resource for a given session + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__document_type = data.get("document_type", None) + self.__issuing_country = data.get("issuing_country", None) + self.__tasks = [TaskResponse(task) for task in data.get("tasks", [])] + self.__pages = [PageResponse(page) for page in data.get("pages", [])] + self.__document_fields = ( + DocumentFieldsResponse(data["document_fields"]) + if "document_fields" in data.keys() + else None + ) + + @property + def id(self): + """ + Returns the ID of the identity document + + :return: the ID + :rtype: str or None + """ + return self.__id + + @property + def tasks(self): + """ + Returns all associated tasks of the identity document + + :return: the associated tasks + :rtype: list[TaskResponse] + """ + return self.__tasks + + @property + def document_type(self): + """ + Returns the identity document type, e.g. "PASSPORT" + + :return: the document type + :rtype: str or None + """ + return self.__document_type + + @property + def issuing_country(self): + """ + Returns the issuing country of the identity document + + :return: the issuing country + :rtype: str or None + """ + return self.__issuing_country + + @property + def pages(self): + """ + Returns the individual pages of the identity document + + :return: the pages + :rtype: list[PageResponse] + """ + return self.__pages + + @property + def document_fields(self): + """ + Returns the associated document fields + + :return: the document fields + :rtype: DocumentFieldsResponse + """ + return self.__document_fields diff --git a/yoti_python_sdk/doc_scan/session/retrieve/liveness_resource_response.py b/yoti_python_sdk/doc_scan/session/retrieve/liveness_resource_response.py new file mode 100644 index 00000000..7f629475 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/liveness_resource_response.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .resource_response import ResourceResponse +from .face_map_response import FaceMapResponse +from .frame_response import FrameResponse + + +class LivenessResourceResponse(ResourceResponse): + """ + Represents a Liveness resource for a given session + """ + + pass + + +class ZoomLivenessResourceResponse(LivenessResourceResponse): + """ + Represents a Zoom Liveness resource for a given session + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + LivenessResourceResponse.__init__(self, data) + + self.__facemap = ( + FaceMapResponse(data["facemap"]) if "facemap" in data.keys() else None + ) + self.__frames = [FrameResponse(frame) for frame in data.get("frames", [])] + + @property + def facemap(self): + """ + Returns the associated facemap information for + the zoom liveness resource + + :return: the facemap + :rtype: FaceMapResponse or None + """ + return self.__facemap + + @property + def frames(self): + """ + Returns the list of associated frames for + the zoom liveness resource + + :return: the frames + :rtype: list[FrameResponse] + """ + return self.__frames diff --git a/yoti_python_sdk/doc_scan/session/retrieve/media_response.py b/yoti_python_sdk/doc_scan/session/retrieve/media_response.py new file mode 100644 index 00000000..995250fd --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/media_response.py @@ -0,0 +1,74 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import iso8601 +from iso8601 import ParseError + + +class MediaResponse(object): + """ + Represents a media resource + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__type = data.get("type", None) + self.__created = self.__parse_date(data.get("created", None)) + self.__last_updated = self.__parse_date(data.get("last_updated", None)) + + @staticmethod + def __parse_date(date): + if date is None: + return date + + try: + return iso8601.parse_date(date) + except ParseError: + return None + + @property + def id(self): + """ + The ID of the media resource + + :return: the ID + :rtype: str or None + """ + return self.__id + + @property + def type(self): + """ + The type of the media resource, e.g. "JSON" + + :return: the type + :rtype: str or None + """ + return self.__type + + @property + def created(self): + """ + The date the media resource was created + + :return: the created date + :rtype: datetime.datetime or None + """ + return self.__created + + @property + def last_updated(self): + """ + The date the media resource was last updated + + :return: the last updated date + :rtype: datetime.datetime or None + """ + return self.__last_updated diff --git a/yoti_python_sdk/doc_scan/session/retrieve/media_value.py b/yoti_python_sdk/doc_scan/session/retrieve/media_value.py new file mode 100644 index 00000000..7bf13458 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/media_value.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import base64 + + +class MediaValue(object): + def __init__(self, content_type, content): + self.__mime_type = content_type + self.__content = content + + @property + def mime_type(self): + return self.__mime_type + + @property + def content(self): + return self.__content + + @property + def base64_content(self): + return "data:%s;base64,%s" % ( + self.mime_type, + base64.b64encode(self.__content).decode("utf-8"), + ) diff --git a/yoti_python_sdk/doc_scan/session/retrieve/page_response.py b/yoti_python_sdk/doc_scan/session/retrieve/page_response.py new file mode 100644 index 00000000..949ae345 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/page_response.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from .media_response import MediaResponse + + +class PageResponse(object): + """ + Represents information about an uploaded document Page + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__capture_method = ( + data["capture_method"] if "capture_method" in data.keys() else None + ) + self.__media = MediaResponse(data["media"]) if "media" in data.keys() else None + + @property + def capture_method(self): + """ + The capture method that was used for the Page + + :return: the capture method + :rtype: str or None + """ + return self.__capture_method + + @property + def media(self): + """ + The media associated with the Page + + :return: the media + :rtype: MediaResponse or None + """ + return self.__media diff --git a/yoti_python_sdk/doc_scan/session/retrieve/recommendation_response.py b/yoti_python_sdk/doc_scan/session/retrieve/recommendation_response.py new file mode 100644 index 00000000..87f8f3f9 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/recommendation_response.py @@ -0,0 +1,46 @@ +class RecommendationResponse(object): + """ + Represents the recommendation given for a check + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__value = data.get("value", None) + self.__reason = data.get("reason", None) + self.__recovery_suggestion = data.get("recovery_suggestion", None) + + @property + def value(self): + """ + Returns the value of the recommendation + + :return: the value + :rtype: str or None + """ + return self.__value + + @property + def reason(self): + """ + Returns the reason of the recommendation + + :return: the reason + :rtype: str or None + """ + return self.__reason + + @property + def recovery_suggestion(self): + """ + Returns the recovery suggestion of the recommendation + + :return: the recovery suggestion + :rtype: str or None + """ + return self.__recovery_suggestion diff --git a/yoti_python_sdk/doc_scan/session/retrieve/report_response.py b/yoti_python_sdk/doc_scan/session/retrieve/report_response.py new file mode 100644 index 00000000..4ab1f45b --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/report_response.py @@ -0,0 +1,46 @@ +from .breakdown_response import BreakdownResponse +from .recommendation_response import RecommendationResponse + + +class ReportResponse(object): + """ + Represents a report for a given check + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__recommendation = ( + RecommendationResponse(data["recommendation"]) + if "recommendation" in data.keys() + else None + ) + + self.__breakdown = [ + BreakdownResponse(breakdown) for breakdown in data.get("breakdown", []) + ] + + @property + def recommendation(self): + """ + The recommendation given for a given check/task + + :return: the recommendation + :rtype: RecommendationResponse + """ + return self.__recommendation + + @property + def breakdown(self): + """ + A list of breakdowns for different sub-checks performed + + :return: the list of breakdowns + :rtype: list[BreakdownResponse] + """ + return self.__breakdown diff --git a/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py b/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py new file mode 100644 index 00000000..ce60cc28 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py @@ -0,0 +1,77 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.session.retrieve.id_document_resource_response import ( + IdDocumentResourceResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.liveness_resource_response import ( + LivenessResourceResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.liveness_resource_response import ( + ZoomLivenessResourceResponse, +) + + +class ResourceContainer(object): + """ + Contains different resources that are part of the Yoti + Doc Scan session + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id_documents = [ + IdDocumentResourceResponse(document) + for document in data.get("id_documents", []) + ] + + self.__liveness_capture = [ + self.__parse_liveness_capture(liveness) + for liveness in data.get("liveness_capture", []) + ] + + @staticmethod + def __parse_liveness_capture(liveness_capture): + """ + Parses a liveness capture into a specific sub-class based on the + liveness type. If no liveness type is available, it falls back + to the parent class :class:`LivenessResourceResponse` + + :param liveness_capture: the liveness capture + :type liveness_capture: dict + :return: the parsed liveness capture + :rtype: LivenessResourceResponse + """ + types = {"ZOOM": ZoomLivenessResourceResponse} + + clazz = types.get( + liveness_capture.get("liveness_type", None), + LivenessResourceResponse, # Fallback value for unknown type + ) + return clazz(liveness_capture) + + @property + def id_documents(self): + """ + Return a list of ID document resources + + :return: list of ID documents + :rtype: list[IdDocumentResourceResponse] + """ + return self.__id_documents + + @property + def liveness_capture(self): + """ + Return a list of liveness capture resources + + :return: list of liveness captures + :rtype: list[LivenessResourceResponse] + """ + return self.__liveness_capture diff --git a/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py b/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py new file mode 100644 index 00000000..f64efcf9 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py @@ -0,0 +1,52 @@ +from yoti_python_sdk.doc_scan import constants +from .task_response import TaskResponse +from .task_response import TextExtractionTaskResponse + + +class ResourceResponse(object): + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__tasks = [self.__parse_task(task) for task in data.get("tasks", [])] + + @staticmethod + def __parse_task(task): + """ + Return a parsed task from a dictionary + + :param task: the raw task + :type task: dict + :return: the parsed task + :rtype: TaskResponse + """ + types = {constants.ID_DOCUMENT_TEXT_DATA_EXTRACTION: TextExtractionTaskResponse} + clazz = types.get( + task.get("type", None), TaskResponse # Default fallback for task type + ) + return clazz(task) + + @property + def id(self): + """ + The ID of the resource + + :return: the id + :rtype: str + """ + return self.__id + + @property + def tasks(self): + """ + Tasks associated with a resource + + :return: the list of tasks + :rtype: list[TaskResponse] + """ + return self.__tasks diff --git a/yoti_python_sdk/doc_scan/session/retrieve/task_response.py b/yoti_python_sdk/doc_scan/session/retrieve/task_response.py new file mode 100644 index 00000000..9511dc4b --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/task_response.py @@ -0,0 +1,159 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +import iso8601 +from iso8601 import ParseError + +from yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.doc_scan.session.retrieve.generated_check_response import ( + GeneratedCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.generated_check_response import ( + GeneratedTextDataCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.generated_media import GeneratedMedia + + +class TaskResponse(object): + """ + Represents a task + """ + + def __init__(self, data=None): + """ + :param data: the data to parse + :type data: dict or None + """ + if data is None: + data = dict() + + self.__id = data.get("id", None) + self.__type = data.get("type", None) + self.__state = data.get("state", None) + + self.__created = self.__parse_date(data.get("created", None)) + self.__last_updated = self.__parse_date(data.get("last_updated", None)) + + self.__generated_checks = [ + self.__parse_generated_check(check) + for check in data.get("generated_checks", []) + ] + self.__generated_media = [ + GeneratedMedia(media) for media in data.get("generated_media", []) + ] + + @staticmethod + def __parse_generated_check(generated_check): + """ + Parse a generated check into an object from a dict + :param generated_check: the raw generated check + :type generated_check: dict + :return: the parse generated check + :rtype: GeneratedCheckResponse + """ + types = {constants.ID_DOCUMENT_TEXT_DATA_CHECK: GeneratedTextDataCheckResponse} + + clazz = types.get( + generated_check.get("type", None), + GeneratedCheckResponse, # Default fallback for type + ) + return clazz(generated_check) + + @staticmethod + def __parse_date(date): + """ + Parse a date using the iso8601 library, + returning None if there was an error + + :param date: the date string to parse + :type date: str + :return: the parse date + :rtype: datetime.datetime or None + """ + if date is None: + return None + + try: + return iso8601.parse_date(date) + except ParseError: + return None + + @property + def type(self): + """ + Return the type of the task + + :return: the type + :rtype: str or None + """ + return self.__type + + @property + def id(self): + """ + Return the ID of the task + + :return: the ID + :rtype: str or None + """ + return self.__id + + @property + def state(self): + """ + Return the state of the task + + :return: the state + :rtype: str or None + """ + return self.__state + + @property + def created(self): + """ + Return the date the task was created + + :return: the created date + :rtype: datetime.datetime or None + """ + return self.__created + + @property + def last_updated(self): + """ + Return the date the task was last updated + + :return: the last updated date + :rtype: datetime.datetime or None + """ + return self.__last_updated + + @property + def generated_checks(self): + """ + Return the list of checks that were generated + by the task + + :return: the generated checks + :rtype: list[GeneratedCheckResponse] + """ + return self.__generated_checks + + @property + def generated_media(self): + """ + Return the list of media that has been generated + by the task + + :return: the list of generated media + :rtype: list[GeneratedMedia] + """ + return self.__generated_media + + +class TextExtractionTaskResponse(TaskResponse): + """ + Represents a Text Extraction task response + """ + + pass diff --git a/yoti_python_sdk/http.py b/yoti_python_sdk/http.py index 09225983..0c8913a3 100644 --- a/yoti_python_sdk/http.py +++ b/yoti_python_sdk/http.py @@ -24,12 +24,17 @@ class YotiResponse(object): - def __init__(self, status_code, text): + def __init__(self, status_code, text, headers=None, content=None): + if headers is None: + headers = {} + self.status_code = status_code self.text = text + self.content = content + self.headers = headers -class RequestHandler: +class RequestHandler(object): """ Default request handler for signing requests using the requests library. This type can be inherited and the execute method overridden to use any @@ -60,7 +65,12 @@ def execute(request): headers=request.headers, ) - return YotiResponse(status_code=response.status_code, text=response.text) + return YotiResponse( + status_code=response.status_code, + text=response.text, + headers=response.headers, + content=response.content, + ) class SignedRequest(object): diff --git a/yoti_python_sdk/tests/conftest.py b/yoti_python_sdk/tests/conftest.py index 335e201e..86f080cf 100644 --- a/yoti_python_sdk/tests/conftest.py +++ b/yoti_python_sdk/tests/conftest.py @@ -1,6 +1,8 @@ # -*- coding: utf-8 -*- import io -from os.path import dirname, join, abspath +from os.path import abspath +from os.path import dirname +from os.path import join import pytest diff --git a/yoti_python_sdk/tests/doc_scan/__init__.py b/yoti_python_sdk/tests/doc_scan/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/doc_scan/conftest.py b/yoti_python_sdk/tests/doc_scan/conftest.py new file mode 100644 index 00000000..5b85abc6 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/conftest.py @@ -0,0 +1,20 @@ +from os.path import abspath +from os.path import dirname +from os.path import join + +import pytest + +from yoti_python_sdk.doc_scan.client import DocScanClient + +FIXTURES_DIR = join(dirname(abspath(__file__)), "..", "fixtures") +PEM_FILE_PATH = join(FIXTURES_DIR, "sdk-test.pem") + +YOTI_CLIENT_SDK_ID = "737204aa-d54e-49a4-8bde-26ddbe6d880c" + + +@pytest.fixture(scope="module") +def doc_scan_client(): + """ + :rtype: DocScanClient + """ + return DocScanClient(YOTI_CLIENT_SDK_ID, PEM_FILE_PATH) diff --git a/yoti_python_sdk/tests/doc_scan/fixtures/failed_request.txt b/yoti_python_sdk/tests/doc_scan/fixtures/failed_request.txt new file mode 100644 index 00000000..9407b369 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/fixtures/failed_request.txt @@ -0,0 +1 @@ +{"error": "MALFORMED_REQUEST"} \ No newline at end of file diff --git a/yoti_python_sdk/tests/doc_scan/fixtures/retrieve_session_success.txt b/yoti_python_sdk/tests/doc_scan/fixtures/retrieve_session_success.txt new file mode 100644 index 00000000..add6510b --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/fixtures/retrieve_session_success.txt @@ -0,0 +1 @@ +{"session_id":"someSessionId"} \ No newline at end of file diff --git a/yoti_python_sdk/tests/doc_scan/fixtures/session_create_success.txt b/yoti_python_sdk/tests/doc_scan/fixtures/session_create_success.txt new file mode 100644 index 00000000..a813cfef --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/fixtures/session_create_success.txt @@ -0,0 +1 @@ +{"session_id": "someSessionId", "client_session_token": "someClientSessionToken", "client_session_token_ttl": 299} \ No newline at end of file diff --git a/yoti_python_sdk/tests/doc_scan/mocks.py b/yoti_python_sdk/tests/doc_scan/mocks.py new file mode 100644 index 00000000..e3be56d1 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/mocks.py @@ -0,0 +1,46 @@ +from os.path import abspath +from os.path import dirname +from os.path import join + +from yoti_python_sdk.tests.mocks import MockResponse + +FIXTURES_DIR = join(dirname(abspath(__file__)), "fixtures") + + +def mocked_request_successful_session_creation(): + with open(FIXTURES_DIR + "/session_create_success.txt", "r") as f: + response = f.read() + return MockResponse(status_code=201, text=response) + + +def mocked_request_failed_session_creation(): + with open(FIXTURES_DIR + "/failed_request.txt", "r") as f: + response = f.read() + return MockResponse(status_code=400, text=response) + + +def mocked_request_successful_session_retrieval(): + with open(FIXTURES_DIR + "/retrieve_session_success.txt", "r") as f: + response = f.read() + return MockResponse(status_code=200, text=response) + + +def mocked_request_failed_session_retrieval(): + return MockResponse(status_code=400, text="") + + +def mocked_request_media_content(): + return MockResponse( + status_code=200, + text="someContent", + content=b"someContent", + headers={"Content-Type": "application/json"}, + ) + + +def mocked_request_missing_content(): + return MockResponse(status_code=404, text="") + + +def mocked_request_server_error(): + return MockResponse(status_code=500, text="") diff --git a/yoti_python_sdk/tests/doc_scan/session/create/check/test_face_match_check.py b/yoti_python_sdk/tests/doc_scan/session/create/check/test_face_match_check.py new file mode 100644 index 00000000..1da0d8f9 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/check/test_face_match_check.py @@ -0,0 +1,44 @@ +import json +import unittest + +from yoti_python_sdk.doc_scan.session.create import RequestedFaceMatchCheckBuilder +from yoti_python_sdk.doc_scan.session.create.check.face_match import ( + RequestedFaceMatchCheck, +) +from yoti_python_sdk.doc_scan.session.create.check.face_match import ( + RequestedFaceMatchCheckConfig, +) +from yoti_python_sdk.doc_scan.session.create.check.requested_check import RequestedCheck +from yoti_python_sdk.utils import YotiEncoder + + +class RequestedFaceMatchCheckTest(unittest.TestCase): + def test_should_build_with_manual_check_always(self): + result = RequestedFaceMatchCheckBuilder().with_manual_check_always().build() + + assert isinstance(result, RequestedCheck) + assert isinstance(result, RequestedFaceMatchCheck) + assert isinstance(result.config, RequestedFaceMatchCheckConfig) + + assert result.type == "ID_DOCUMENT_FACE_MATCH" + assert result.config.manual_check == "ALWAYS" + + def test_should_build_with_manual_check_fallback(self): + result = RequestedFaceMatchCheckBuilder().with_manual_check_fallback().build() + + assert result.config.manual_check == "FALLBACK" + + def test_should_build_with_manual_check_never(self): + result = RequestedFaceMatchCheckBuilder().with_manual_check_never().build() + + assert result.config.manual_check == "NEVER" + + def test_should_serialize_to_json_without_error(self): + result = RequestedFaceMatchCheckBuilder().with_manual_check_never().build() + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py b/yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py new file mode 100644 index 00000000..130deb69 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py @@ -0,0 +1,57 @@ +import unittest +import json + +from yoti_python_sdk.doc_scan.session.create.check import RequestedLivenessCheckBuilder +from yoti_python_sdk.doc_scan.session.create.check.liveness import ( + RequestedLivenessCheck, +) +from yoti_python_sdk.doc_scan.session.create.check.liveness import ( + RequestedLivenessCheckConfig, +) +from yoti_python_sdk.doc_scan.session.create.check.requested_check import RequestedCheck +from yoti_python_sdk.utils import YotiEncoder + + +class RequestedLivenessCheckTest(unittest.TestCase): + def test_should_build_correctly(self): + result = ( + RequestedLivenessCheckBuilder() + .with_liveness_type("SOME_LIVENESS_TYPE") + .with_max_retries(3) + .build() + ) + + assert isinstance(result, RequestedCheck) + assert isinstance(result, RequestedLivenessCheck) + assert isinstance(result.config, RequestedLivenessCheckConfig) + + assert result.type == "LIVENESS" + assert result.config.liveness_type == "SOME_LIVENESS_TYPE" + assert result.config.max_retries == 3 + + def test_should_build_with_zoom_liveness_type(self): + result = ( + RequestedLivenessCheckBuilder() + .for_zoom_liveness() + .with_max_retries(5) + .build() + ) + + assert result.type == "LIVENESS" + assert result.config.liveness_type == "ZOOM" + assert result.config.max_retries == 5 + + def test_should_serialize_to_json_without_error(self): + result = ( + RequestedLivenessCheckBuilder() + .for_zoom_liveness() + .with_max_retries(5) + .build() + ) + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/check/test_requested_document_authenticity_check.py b/yoti_python_sdk/tests/doc_scan/session/create/check/test_requested_document_authenticity_check.py new file mode 100644 index 00000000..ae0d0596 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/check/test_requested_document_authenticity_check.py @@ -0,0 +1,34 @@ +import json +import unittest + +from yoti_python_sdk.doc_scan.session.create.check import ( + RequestedDocumentAuthenticityCheckBuilder, +) +from yoti_python_sdk.doc_scan.session.create.check.document_authenticity import ( + RequestedDocumentAuthenticityCheck, +) +from yoti_python_sdk.doc_scan.session.create.check.document_authenticity import ( + RequestedDocumentAuthenticityCheckConfig, +) +from yoti_python_sdk.doc_scan.session.create.check.requested_check import RequestedCheck +from yoti_python_sdk.utils import YotiEncoder + + +class RequestedDocumentAuthenticityCheckTest(unittest.TestCase): + def test_should_build_correctly(self): + result = RequestedDocumentAuthenticityCheckBuilder().build() + + assert isinstance(result, RequestedCheck) + assert isinstance(result, RequestedDocumentAuthenticityCheck) + assert isinstance(result.config, RequestedDocumentAuthenticityCheckConfig) + assert result.type == "ID_DOCUMENT_AUTHENTICITY" + + def test_should_serialize_to_json_without_error(self): + result = RequestedDocumentAuthenticityCheckBuilder().build() + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/task/test_text_extraction_task.py b/yoti_python_sdk/tests/doc_scan/session/create/task/test_text_extraction_task.py new file mode 100644 index 00000000..2d20ae47 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/task/test_text_extraction_task.py @@ -0,0 +1,48 @@ +import json +import unittest + +from yoti_python_sdk.doc_scan.session.create.task import ( + RequestedTextExtractionTaskBuilder, +) +from yoti_python_sdk.doc_scan.session.create.task.requested_task import RequestedTask +from yoti_python_sdk.doc_scan.session.create.task.text_extraction import ( + RequestedTextExtractionTask, +) +from yoti_python_sdk.doc_scan.session.create.task.text_extraction import ( + RequestedTextExtractionTaskConfig, +) +from yoti_python_sdk.utils import YotiEncoder + + +class RequestedTextExtractionTaskTest(unittest.TestCase): + def test_should_build_with_manual_check_always(self): + result = RequestedTextExtractionTaskBuilder().with_manual_check_always().build() + + assert isinstance(result, RequestedTask) + assert isinstance(result, RequestedTextExtractionTask) + assert isinstance(result.config, RequestedTextExtractionTaskConfig) + + assert result.type == "ID_DOCUMENT_TEXT_DATA_EXTRACTION" + assert result.config.manual_check == "ALWAYS" + + def test_should_build_with_manual_check_fallback(self): + result = ( + RequestedTextExtractionTaskBuilder().with_manual_check_fallback().build() + ) + + assert result.config.manual_check == "FALLBACK" + + def test_should_build_with_manual_check_never(self): + result = RequestedTextExtractionTaskBuilder().with_manual_check_never().build() + + assert result.config.manual_check == "NEVER" + + def test_should_serialize_to_json_without_error(self): + result = RequestedTextExtractionTaskBuilder().with_manual_check_never().build() + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/test_notification_config.py b/yoti_python_sdk/tests/doc_scan/session/create/test_notification_config.py new file mode 100644 index 00000000..f207ff14 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/test_notification_config.py @@ -0,0 +1,130 @@ +import json +import unittest + +from yoti_python_sdk.doc_scan.session.create import NotificationConfigBuilder +from yoti_python_sdk.doc_scan.session.create.notification_config import ( + NotificationConfig, +) +from yoti_python_sdk.utils import YotiEncoder + + +class NotificationConfigTest(unittest.TestCase): + SOME_AUTH_TOKEN = "someAuthToken" + SOME_ENDPOINT = "someEndpoint" + SOME_TOPIC = "someTopic" + + def test_should_build_correctly(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .with_topic(self.SOME_TOPIC) + .build() + ) + + assert isinstance(result, NotificationConfig) + assert result.auth_token is self.SOME_AUTH_TOKEN + assert result.endpoint is self.SOME_ENDPOINT + assert self.SOME_TOPIC in result.topics + + def test_should_add_resource_update_topic(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .for_resource_update() + .build() + ) + + assert "RESOURCE_UPDATE" in result.topics + + def test_should_add_task_completion_topic(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .for_task_completion() + .build() + ) + + assert "TASK_COMPLETION" in result.topics + + def test_should_add_session_completion_topic(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .for_session_completion() + .build() + ) + + assert "SESSION_COMPLETION" in result.topics + + def test_should_add_check_completion_topic(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .for_check_completion() + .build() + ) + + assert "CHECK_COMPLETION" in result.topics + + def test_should_allow_multiple_topics(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .for_resource_update() + .for_task_completion() + .for_session_completion() + .for_check_completion() + .build() + ) + + expected = [ + "RESOURCE_UPDATE", + "TASK_COMPLETION", + "SESSION_COMPLETION", + "CHECK_COMPLETION", + ] + assert all(x in result.topics for x in expected) + + def test_should_store_unique_topics(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .for_resource_update() + .for_resource_update() + .for_resource_update() + .build() + ) + + assert len(result.topics) == 1 + + def test_should_serialize_to_json_without_error(self): + result = ( + NotificationConfigBuilder() + .with_auth_token(self.SOME_AUTH_TOKEN) + .with_endpoint(self.SOME_ENDPOINT) + .for_resource_update() + .for_task_completion() + .for_session_completion() + .for_check_completion() + .build() + ) + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + def test_topics_should_default_to_empty_list_if_none(self): + result = NotificationConfig("someAuthToken", "someEndpoint", None) + + assert isinstance(result.topics, list) + assert len(result.topics) == 0 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py b/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py new file mode 100644 index 00000000..6de7b4ca --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py @@ -0,0 +1,66 @@ +import json +import unittest + +from yoti_python_sdk.doc_scan.session.create import SdkConfigBuilder +from yoti_python_sdk.doc_scan.session.create.sdk_config import SdkConfig +from yoti_python_sdk.utils import YotiEncoder + + +class SdkConfigTest(unittest.TestCase): + SOME_PRIMARY_COLOUR = "#77355f" + SOME_SECONDARY_COLOUR = "#5bfc31" + SOME_FONT_COLOUR = "#60f021" + SOME_LOCALE = "en" + SOME_PRESET_ISSUING_COUNTRY = "USA" + SOME_SUCCESS_URL = "https://mysite.com/yoti/success" + SOME_ERROR_URL = "https://mysite.com/yoti/error" + + def test_should_build_correctly(self): + result = ( + SdkConfigBuilder() + .with_allows_camera_and_upload() + .with_primary_colour(self.SOME_PRIMARY_COLOUR) + .with_secondary_colour(self.SOME_SECONDARY_COLOUR) + .with_font_colour(self.SOME_FONT_COLOUR) + .with_locale(self.SOME_LOCALE) + .with_preset_issuing_country(self.SOME_PRESET_ISSUING_COUNTRY) + .with_success_url(self.SOME_SUCCESS_URL) + .with_error_url(self.SOME_ERROR_URL) + .build() + ) + + assert isinstance(result, SdkConfig) + assert result.allowed_capture_methods == "CAMERA_AND_UPLOAD" + assert result.primary_colour is self.SOME_PRIMARY_COLOUR + assert result.secondary_colour is self.SOME_SECONDARY_COLOUR + assert result.font_colour is self.SOME_FONT_COLOUR + assert result.locale is self.SOME_LOCALE + assert result.preset_issuing_country is self.SOME_PRESET_ISSUING_COUNTRY + assert result.success_url is self.SOME_SUCCESS_URL + assert result.error_url is self.SOME_ERROR_URL + + def test_should_allows_camera(self): + result = SdkConfigBuilder().with_allows_camera().build() + + assert result.allowed_capture_methods == "CAMERA" + + def test_should_serialize_to_json_without_error(self): + result = ( + SdkConfigBuilder() + .with_allows_camera_and_upload() + .with_primary_colour(self.SOME_PRIMARY_COLOUR) + .with_secondary_colour(self.SOME_SECONDARY_COLOUR) + .with_font_colour(self.SOME_FONT_COLOUR) + .with_locale(self.SOME_LOCALE) + .with_preset_issuing_country(self.SOME_PRESET_ISSUING_COUNTRY) + .with_success_url(self.SOME_SUCCESS_URL) + .with_error_url(self.SOME_ERROR_URL) + .build() + ) + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/test_session_spec.py b/yoti_python_sdk/tests/doc_scan/session/create/test_session_spec.py new file mode 100644 index 00000000..b63ae1a3 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/test_session_spec.py @@ -0,0 +1,79 @@ +import json +import unittest + +from mock import Mock + +from yoti_python_sdk.doc_scan.session.create import SessionSpecBuilder +from yoti_python_sdk.doc_scan.session.create.check.requested_check import RequestedCheck +from yoti_python_sdk.doc_scan.session.create.notification_config import ( + NotificationConfig, +) +from yoti_python_sdk.doc_scan.session.create.sdk_config import SdkConfig +from yoti_python_sdk.doc_scan.session.create.task.requested_task import RequestedTask +from yoti_python_sdk.utils import YotiEncoder + + +class SessionSpecTest(unittest.TestCase): + SOME_CLIENT_SESSION_TOKEN_TTL = 300 + SOME_RESOURCES_TTL = 100000 + SOME_USER_TRACKING_ID = "someUserTrackingId" + + def test_should_build_correctly(self): + sdk_config_mock = Mock(spec=SdkConfig) + notification_mock = Mock(spec=NotificationConfig) + requested_check_mock = Mock(spec=RequestedCheck) + requested_task_mock = Mock(spec=RequestedTask) + + result = ( + SessionSpecBuilder() + .with_client_session_token_ttl(self.SOME_CLIENT_SESSION_TOKEN_TTL) + .with_resources_ttl(self.SOME_RESOURCES_TTL) + .with_user_tracking_id(self.SOME_USER_TRACKING_ID) + .with_notifications(notification_mock) + .with_sdk_config(sdk_config_mock) + .with_requested_check(requested_check_mock) + .with_requested_task(requested_task_mock) + .build() + ) + + assert result.client_session_token_ttl is self.SOME_CLIENT_SESSION_TOKEN_TTL + assert result.resources_ttl is self.SOME_RESOURCES_TTL + assert result.user_tracking_id is self.SOME_USER_TRACKING_ID + assert result.sdk_config is sdk_config_mock + assert result.notifications is notification_mock + assert len(result.requested_checks) == 1 + assert requested_check_mock in result.requested_checks + assert len(result.requested_tasks) == 1 + assert requested_task_mock in result.requested_tasks + + def test_should_serialize_to_json_without_error(self): + sdk_config_mock = Mock(spec=SdkConfig) + sdk_config_mock.to_json.return_value = {} + + notification_mock = Mock(spec=NotificationConfig) + notification_mock.to_json.return_value = {} + + requested_check_mock = Mock(spec=RequestedCheck) + requested_check_mock.to_json.return_value = {} + + requested_task_mock = Mock(spec=RequestedTask) + requested_task_mock.to_json.return_value = {} + + result = ( + SessionSpecBuilder() + .with_client_session_token_ttl(self.SOME_CLIENT_SESSION_TOKEN_TTL) + .with_resources_ttl(self.SOME_RESOURCES_TTL) + .with_user_tracking_id(self.SOME_USER_TRACKING_ID) + .with_notifications(notification_mock) + .with_sdk_config(sdk_config_mock) + .with_requested_check(requested_check_mock) + .with_requested_task(requested_task_mock) + .build() + ) + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/__init__.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_breakdown_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_breakdown_response.py new file mode 100644 index 00000000..2b534512 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_breakdown_response.py @@ -0,0 +1,37 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.breakdown_response import ( + BreakdownResponse, +) + + +class BreakdownResponseTest(unittest.TestCase): + SOME_SUB_CHECK = "someSubCheck" + SOME_RESULT = "someResult" + SOME_DETAILS = [ + {"name": "firstDetailName", "value": "firstDetailValue"}, + {"name": "secondDetailName", "value": "secondDetailValue"}, + ] + + def test_should_build_correctly(self): + data = { + "sub_check": self.SOME_SUB_CHECK, + "result": self.SOME_RESULT, + "details": self.SOME_DETAILS, + } + + result = BreakdownResponse(data) + + assert result.sub_check is self.SOME_SUB_CHECK + assert result.result is self.SOME_RESULT + assert len(result.details) == 2 + assert result.details[0].name == "firstDetailName" + assert result.details[0].value == "firstDetailValue" + + def test_should_default_details_to_empty_list(self): + result = BreakdownResponse({}) + assert len(result.details) == 0 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_check_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_check_response.py new file mode 100644 index 00000000..a3ce2318 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_check_response.py @@ -0,0 +1,85 @@ +import unittest +from datetime import datetime + +import pytz + +from yoti_python_sdk.doc_scan.session.retrieve.check_response import CheckResponse +from yoti_python_sdk.doc_scan.session.retrieve.generated_media import GeneratedMedia +from yoti_python_sdk.doc_scan.session.retrieve.report_response import ReportResponse + + +class CheckResponseTest(unittest.TestCase): + SOME_ID = "someId" + SOME_STATE = "someState" + SOME_TYPE = "someType" + SOME_RESOURCES_USED = ["someFirstId", "someSecondId"] + SOME_REPORT = {} + SOME_GENERATED_MEDIA = [{"someKey": "someValue"}] + SOME_CREATED = "2019-05-01T05:01:48.000Z" + SOME_LAST_UPDATED = "2019-05-01T05:01:48.000Z" + + EXPECTED_DATETIME = datetime( + year=2019, + month=5, + day=1, + hour=5, + minute=1, + second=48, + microsecond=0, + tzinfo=pytz.utc, + ) + + def test_should_build_correctly(self): + data = { + "id": self.SOME_ID, + "state": self.SOME_STATE, + "type": self.SOME_TYPE, + "resources_used": self.SOME_RESOURCES_USED, + "report": self.SOME_REPORT, + "generated_media": self.SOME_GENERATED_MEDIA, + "created": self.SOME_CREATED, + "last_updated": self.SOME_LAST_UPDATED, + } + + result = CheckResponse(data) + + assert result.id is self.SOME_ID + assert result.state is self.SOME_STATE + assert result.type is self.SOME_TYPE + assert len(result.resources_used) == 2 + assert isinstance(result.report, ReportResponse) + assert len(result.generated_media) == 1 + assert isinstance(result.generated_media[0], GeneratedMedia) + assert isinstance(result.created, datetime) + + assert result.created == self.EXPECTED_DATETIME + assert result.last_updated == self.EXPECTED_DATETIME + + def test_should_default_relevant_properties_to_empty_list(self): + result = CheckResponse({}) + + assert len(result.resources_used) == 0 + assert len(result.generated_media) == 0 + + def test_should_default_data_if_none(self): + result = CheckResponse(None) + + assert result.id is None + assert result.state is None + assert result.type is None + assert result.created is None + assert result.last_updated is None + assert len(result.resources_used) == 0 + assert len(result.generated_media) == 0 + + def test_should_set_dates_to_none_if_invalid_format(self): + data = {"created": "someInvalidDate", "last_updated": "someInvalidDate"} + + result = CheckResponse(data) + + assert result.created is None + assert result.last_updated is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_create_session_result.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_create_session_result.py new file mode 100644 index 00000000..13c6b615 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_create_session_result.py @@ -0,0 +1,35 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.create_session_result import ( + CreateSessionResult, +) + + +class CreateSessionResultTest(unittest.TestCase): + SOME_CLIENT_SESSION_TOKEN_TTL = 300 + SOME_CLIENT_SESSION_TOKEN = "someClientSessionToken" + SOME_SESSION_ID = "someSessionId" + + def test_should_build_correctly(self): + data = { + "client_session_token_ttl": self.SOME_CLIENT_SESSION_TOKEN_TTL, + "client_session_token": self.SOME_CLIENT_SESSION_TOKEN, + "session_id": self.SOME_SESSION_ID, + } + + result = CreateSessionResult(data) + + assert result.client_session_token_ttl is self.SOME_CLIENT_SESSION_TOKEN_TTL + assert result.client_session_token is self.SOME_CLIENT_SESSION_TOKEN + assert result.session_id is self.SOME_SESSION_ID + + def test_should_parse_when_given_none(self): + result = CreateSessionResult(None) + + assert result.client_session_token_ttl is None + assert result.client_session_token is None + assert result.session_id is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_document_fields_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_document_fields_response.py new file mode 100644 index 00000000..c34e6faf --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_document_fields_response.py @@ -0,0 +1,22 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.document_fields_response import ( + DocumentFieldsResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class DocumentFieldsResponseTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = {"media": {}} + + result = DocumentFieldsResponse(data) + assert isinstance(result.media, MediaResponse) + + def test_should_not_throw_exception_for_none(self): + result = DocumentFieldsResponse(None) + assert result.media is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_face_map_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_face_map_response.py new file mode 100644 index 00000000..5d7e2099 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_face_map_response.py @@ -0,0 +1,20 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.face_map_response import FaceMapResponse +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class FaceMapResponseTest(unittest.TestCase): + def test_should_build_correctly(self): + data = {"media": {}} + + result = FaceMapResponse(data) + assert isinstance(result.media, MediaResponse) + + def test_should_parse_with_none(self): + result = FaceMapResponse(None) + assert result.media is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_frame_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_frame_response.py new file mode 100644 index 00000000..1a4868c0 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_frame_response.py @@ -0,0 +1,21 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.frame_response import FrameResponse +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class FrameResponseTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = {"media": {}} + + result = FrameResponse(data) + assert isinstance(result.media, MediaResponse) + + def test_should_parse_when_none(self): + result = FrameResponse(None) + assert isinstance(result, FrameResponse) + assert result.media is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_generated_check_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_generated_check_response.py new file mode 100644 index 00000000..365673e2 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_generated_check_response.py @@ -0,0 +1,29 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.generated_check_response import ( + GeneratedCheckResponse, +) + + +class GeneratedCheckResponseTest(unittest.TestCase): + SOME_ID = "someId" + SOME_TYPE = "someType" + + def test_should_parse_correctly(self): + data = {"id": self.SOME_ID, "type": self.SOME_TYPE} + + result = GeneratedCheckResponse(data) + + assert result.id is self.SOME_ID + assert result.type is self.SOME_TYPE + + def test_should_parse_when_none(self): + result = GeneratedCheckResponse(None) + + assert isinstance(result, GeneratedCheckResponse) + assert result.id is None + assert result.type is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_generated_media.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_generated_media.py new file mode 100644 index 00000000..9b6140e7 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_generated_media.py @@ -0,0 +1,27 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.generated_media import GeneratedMedia + + +class GeneratedMediaTest(unittest.TestCase): + SOME_ID = "someId" + SOME_TYPE = "someType" + + def test_should_parse_correctly(self): + data = {"id": self.SOME_ID, "type": self.SOME_TYPE} + + result = GeneratedMedia(data) + + assert result.id is self.SOME_ID + assert result.type is self.SOME_TYPE + + def test_should_parse_with_none(self): + result = GeneratedMedia(None) + + assert isinstance(result, GeneratedMedia) + assert result.id is None + assert result.type is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_get_session_result.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_get_session_result.py new file mode 100644 index 00000000..0b259771 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_get_session_result.py @@ -0,0 +1,76 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( + AuthenticityCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( + FaceMatchCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( + LivenessCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( + TextDataCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.get_session_result import ( + GetSessionResult, +) +from yoti_python_sdk.doc_scan.session.retrieve.resource_container import ( + ResourceContainer, +) + + +class GetSessionResultTest(unittest.TestCase): + SOME_CLIENT_SESSION_TOKEN_TTL = 300 + SOME_SESSION_ID = "someSessionId" + SOME_USER_TRACKING_ID = "someUserTrackingId" + SOME_STATE = "someState" + SOME_CLIENT_SESSION_TOKEN = "someClientSessionToken" + SOME_CHECKS = [ + {"type": "ID_DOCUMENT_AUTHENTICITY"}, + {"type": "ID_DOCUMENT_TEXT_DATA_CHECK"}, + {"type": "ID_DOCUMENT_FACE_MATCH"}, + {"type": "LIVENESS"}, + ] + + def test_should_parse_different_checks(self): + data = { + "client_session_token_ttl": self.SOME_CLIENT_SESSION_TOKEN_TTL, + "client_session_token": self.SOME_CLIENT_SESSION_TOKEN, + "session_id": self.SOME_SESSION_ID, + "state": self.SOME_STATE, + "user_tracking_id": self.SOME_USER_TRACKING_ID, + "checks": self.SOME_CHECKS, + "resources": {}, + } + + result = GetSessionResult(data) + + assert result.client_session_token_ttl is self.SOME_CLIENT_SESSION_TOKEN_TTL + assert result.client_session_token is self.SOME_CLIENT_SESSION_TOKEN + assert result.session_id is self.SOME_SESSION_ID + assert result.state is self.SOME_STATE + assert result.user_tracking_id is self.SOME_USER_TRACKING_ID + + assert len(result.checks) == 4 + assert isinstance(result.checks[0], AuthenticityCheckResponse) + assert isinstance(result.checks[1], TextDataCheckResponse) + assert isinstance(result.checks[2], FaceMatchCheckResponse) + assert isinstance(result.checks[3], LivenessCheckResponse) + + assert isinstance(result.resources, ResourceContainer) + + def test_should_filter_checks(self): + data = {"checks": self.SOME_CHECKS} + + result = GetSessionResult(data) + + assert len(result.checks) == 4 + assert len(result.authenticity_checks) == 1 + assert len(result.face_match_checks) == 1 + assert len(result.liveness_checks) == 1 + assert len(result.text_data_checks) == 1 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_id_document_resource_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_id_document_resource_response.py new file mode 100644 index 00000000..630ab264 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_id_document_resource_response.py @@ -0,0 +1,50 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.document_fields_response import ( + DocumentFieldsResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.id_document_resource_response import ( + IdDocumentResourceResponse, +) + + +class IdDocumentResourceResponseTest(unittest.TestCase): + SOME_ID = "someId" + SOME_DOCUMENT_TYPE = "someDocumentType" + SOME_ISSUING_COUNTRY = "someIssuingCountry" + SOME_TASKS = [{"first": "task"}, {"second": "task"}] + SOME_PAGES = [{"first": "page"}, {"second": "page"}] + SOME_DOCUMENT_FIELDS = {"media": {}} + + def test_should_parse_correctly(self): + data = { + "id": self.SOME_ID, + "document_type": self.SOME_DOCUMENT_TYPE, + "issuing_country": self.SOME_ISSUING_COUNTRY, + "tasks": self.SOME_TASKS, + "pages": self.SOME_PAGES, + "document_fields": self.SOME_DOCUMENT_FIELDS, + } + + result = IdDocumentResourceResponse(data) + + assert result.id is self.SOME_ID + assert result.document_type is self.SOME_DOCUMENT_TYPE + assert result.issuing_country is self.SOME_ISSUING_COUNTRY + assert len(result.tasks) == 2 + assert len(result.pages) == 2 + assert isinstance(result.document_fields, DocumentFieldsResponse) + + def test_should_parse_when_none(self): + result = IdDocumentResourceResponse(None) + + assert result.id is None + assert result.document_type is None + assert result.issuing_country is None + assert len(result.tasks) == 0 + assert len(result.pages) == 0 + assert result.document_fields is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_liveness_resource_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_liveness_resource_response.py new file mode 100644 index 00000000..78802b07 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_liveness_resource_response.py @@ -0,0 +1,32 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.face_map_response import FaceMapResponse +from yoti_python_sdk.doc_scan.session.retrieve.liveness_resource_response import ( + ZoomLivenessResourceResponse, +) + + +class LivenessResourceResponseTest(unittest.TestCase): + SOME_ID = "someId" + SOME_FRAMES = [{"first": "frame"}, {"second": "frame"}] + + def test_zoom_liveness_should_parse_correctly(self): + data = {"id": self.SOME_ID, "facemap": {}, "frames": self.SOME_FRAMES} + + result = ZoomLivenessResourceResponse(data) + + assert result.id is self.SOME_ID + assert isinstance(result.facemap, FaceMapResponse) + assert len(result.frames) == 2 + + def test_should_parse_with_none(self): + result = ZoomLivenessResourceResponse(None) + + assert result.id is None + assert len(result.tasks) == 0 + assert result.facemap is None + assert len(result.frames) == 0 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_media_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_media_response.py new file mode 100644 index 00000000..2e9af3db --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_media_response.py @@ -0,0 +1,59 @@ +import unittest +from datetime import datetime + +import pytz + +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class MediaResponseTest(unittest.TestCase): + SOME_ID = "someId" + SOME_TYPE = "someType" + SOME_CREATED = "2019-05-01T05:01:48.000Z" + SOME_LAST_UPDATED = "2019-05-01T05:01:48.000Z" + + EXPECTED_DATETIME = datetime( + year=2019, + month=5, + day=1, + hour=5, + minute=1, + second=48, + microsecond=0, + tzinfo=pytz.utc, + ) + + def test_should_parse_correctly(self): + data = { + "id": self.SOME_ID, + "type": self.SOME_TYPE, + "created": self.SOME_CREATED, + "last_updated": self.SOME_LAST_UPDATED, + } + + result = MediaResponse(data) + + assert result.id is self.SOME_ID + assert result.type is self.SOME_TYPE + assert result.created == self.EXPECTED_DATETIME + assert result.last_updated == self.EXPECTED_DATETIME + + def test_should_parse_with_none(self): + result = MediaResponse(None) + + assert result.id is None + assert result.type is None + assert result.created is None + assert result.last_updated is None + + def test_should_set_dates_as_none_for_invalid_format(self): + data = {"created": "someInvalidFormat", "last_updated": "someInvalidFormat"} + + result = MediaResponse(data) + + assert result.created is None + assert result.last_updated is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_media_value.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_media_value.py new file mode 100644 index 00000000..664f553e --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_media_value.py @@ -0,0 +1,28 @@ +import base64 +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.media_value import MediaValue + + +class MediaValueTest(unittest.TestCase): + SOME_MIME_TYPE = "someMimeType" + SOME_CONTENT = b"someByteArray" + + def test_should_parse_correctly(self): + result = MediaValue(self.SOME_MIME_TYPE, self.SOME_CONTENT) + + assert result.mime_type is self.SOME_MIME_TYPE + assert result.content is self.SOME_CONTENT + + expected = ( + "data:" + + self.SOME_MIME_TYPE + + ";base64," + + base64.b64encode(self.SOME_CONTENT).decode("utf-8") + ) + + assert result.base64_content == expected + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_page_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_page_response.py new file mode 100644 index 00000000..07214d3e --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_page_response.py @@ -0,0 +1,26 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse +from yoti_python_sdk.doc_scan.session.retrieve.page_response import PageResponse + + +class PageResponseTest(unittest.TestCase): + SOME_CAPTURE_METHOD = "someCaptureMethod" + + def test_should_parse_correctly(self): + data = {"capture_method": self.SOME_CAPTURE_METHOD, "media": {}} + + result = PageResponse(data) + + assert result.capture_method is self.SOME_CAPTURE_METHOD + assert isinstance(result.media, MediaResponse) + + def test_should_parse_with_none(self): + result = PageResponse(None) + + assert result.capture_method is None + assert result.media is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_recommendation_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_recommendation_response.py new file mode 100644 index 00000000..43b923f3 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_recommendation_response.py @@ -0,0 +1,35 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.recommendation_response import ( + RecommendationResponse, +) + + +class RecommendationResponseTest(unittest.TestCase): + SOME_VALUE = "someValue" + SOME_REASON = "someReason" + SOME_RECOVERY_SUGGESTION = "someRecoverySuggestion" + + def test_should_parse_correctly(self): + data = { + "value": self.SOME_VALUE, + "reason": self.SOME_REASON, + "recovery_suggestion": self.SOME_RECOVERY_SUGGESTION, + } + + result = RecommendationResponse(data) + + assert result.value is self.SOME_VALUE + assert result.reason is self.SOME_REASON + assert result.recovery_suggestion is self.SOME_RECOVERY_SUGGESTION + + def test_should_parse_with_none(self): + result = RecommendationResponse(None) + + assert result.value is None + assert result.reason is None + assert result.recovery_suggestion is None + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_report_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_report_response.py new file mode 100644 index 00000000..e9468c80 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_report_response.py @@ -0,0 +1,29 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.recommendation_response import ( + RecommendationResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.report_response import ReportResponse + + +class ReportResponseTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = { + "recommendation": {"some": "recommendation"}, + "breakdown": [{"first": "breakdown"}, {"second": "breakdown"}], + } + + result = ReportResponse(data) + + assert isinstance(result.recommendation, RecommendationResponse) + assert len(result.breakdown) == 2 + + def test_should_parse_with_none(self): + result = ReportResponse(None) + + assert result.recommendation is None + assert len(result.breakdown) == 0 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py new file mode 100644 index 00000000..b1507953 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py @@ -0,0 +1,39 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.liveness_resource_response import ( + LivenessResourceResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.liveness_resource_response import ( + ZoomLivenessResourceResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.resource_container import ( + ResourceContainer, +) + + +class ResourceContainerTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = { + "id_documents": [{"first": "id_document"}, {"second": "id_document"}], + "liveness_capture": [ + {"liveness_type": "ZOOM"}, + {"liveness_type": "someUnknown"}, + ], + } + + result = ResourceContainer(data) + + assert len(result.id_documents) == 2 + assert len(result.liveness_capture) == 2 + assert isinstance(result.liveness_capture[0], ZoomLivenessResourceResponse) + assert isinstance(result.liveness_capture[1], LivenessResourceResponse) + + def test_should_parse_with_none(self): + result = ResourceContainer(None) + + assert len(result.id_documents) == 0 + assert len(result.liveness_capture) == 0 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_response.py new file mode 100644 index 00000000..c052a260 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_response.py @@ -0,0 +1,37 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.resource_response import ResourceResponse +from yoti_python_sdk.doc_scan.session.retrieve.task_response import TaskResponse +from yoti_python_sdk.doc_scan.session.retrieve.task_response import ( + TextExtractionTaskResponse, +) + + +class ResourceResponseTest(unittest.TestCase): + SOME_ID = "someId" + + def test_should_parse_correctly(self): + data = { + "id": self.SOME_ID, + "tasks": [ + {"type": "ID_DOCUMENT_TEXT_DATA_EXTRACTION"}, + {"type": "someUnknownType"}, + ], + } + + result = ResourceResponse(data) + + assert result.id is self.SOME_ID + assert len(result.tasks) == 2 + assert isinstance(result.tasks[0], TextExtractionTaskResponse) + assert isinstance(result.tasks[1], TaskResponse) + + def test_should_parse_with_none(self): + result = ResourceResponse(None) + + assert result.id is None + assert len(result.tasks) == 0 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_task_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_task_response.py new file mode 100644 index 00000000..5ef4029e --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_task_response.py @@ -0,0 +1,79 @@ +import unittest +from datetime import datetime + +import pytz + +from yoti_python_sdk.doc_scan.session.retrieve.generated_check_response import ( + GeneratedCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.generated_check_response import ( + GeneratedTextDataCheckResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.task_response import TaskResponse + + +class TaskResponseTest(unittest.TestCase): + SOME_ID = "someId" + SOME_TYPE = "someType" + SOME_STATE = "someState" + + SOME_GENERATED_CHECKS = [ + {"type": "ID_DOCUMENT_TEXT_DATA_CHECK"}, + {"type": "someUnknownType"}, + ] + + SOME_GENERATED_MEDIA = [{"first": "generated_media"}, {"second": "generated_media"}] + + SOME_CREATED = "2019-05-01T05:01:48.000Z" + SOME_LAST_UPDATED = "2019-05-01T05:01:48.000Z" + + EXPECTED_DATETIME = datetime( + year=2019, + month=5, + day=1, + hour=5, + minute=1, + second=48, + microsecond=0, + tzinfo=pytz.utc, + ) + + def test_should_parse_correctly(self): + data = { + "id": self.SOME_ID, + "type": self.SOME_TYPE, + "state": self.SOME_STATE, + "created": self.SOME_CREATED, + "last_updated": self.SOME_LAST_UPDATED, + "generated_checks": self.SOME_GENERATED_CHECKS, + "generated_media": self.SOME_GENERATED_MEDIA, + } + + result = TaskResponse(data) + + assert result.id is self.SOME_ID + assert result.type is self.SOME_TYPE + assert result.state is self.SOME_STATE + assert result.created == self.EXPECTED_DATETIME + assert result.last_updated == self.EXPECTED_DATETIME + + assert len(result.generated_checks) == 2 + assert isinstance(result.generated_checks[0], GeneratedTextDataCheckResponse) + assert isinstance(result.generated_checks[1], GeneratedCheckResponse) + + assert len(result.generated_media) == 2 + + def test_should_parse_with_none(self): + result = TaskResponse(None) + + assert result.id is None + assert result.type is None + assert result.state is None + assert result.created is None + assert result.last_updated is None + assert len(result.generated_checks) == 0 + assert len(result.generated_media) == 0 + + +if __name__ == "__main__": + unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/test_doc_scan_client.py b/yoti_python_sdk/tests/doc_scan/test_doc_scan_client.py new file mode 100644 index 00000000..b47c89df --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/test_doc_scan_client.py @@ -0,0 +1,156 @@ +import pytest + +from yoti_python_sdk.doc_scan.client import DocScanClient # noqa: F401 +from yoti_python_sdk.doc_scan.exception import DocScanException +from yoti_python_sdk.doc_scan.session.create.session_spec import SessionSpec +from yoti_python_sdk.doc_scan.session.retrieve.create_session_result import ( + CreateSessionResult, +) +from yoti_python_sdk.doc_scan.session.retrieve.get_session_result import ( + GetSessionResult, +) +from yoti_python_sdk.tests.doc_scan.mocks import mocked_request_failed_session_creation +from yoti_python_sdk.tests.doc_scan.mocks import mocked_request_failed_session_retrieval +from yoti_python_sdk.tests.doc_scan.mocks import mocked_request_media_content +from yoti_python_sdk.tests.doc_scan.mocks import mocked_request_missing_content +from yoti_python_sdk.tests.doc_scan.mocks import mocked_request_server_error +from yoti_python_sdk.tests.doc_scan.mocks import ( + mocked_request_successful_session_creation, +) +from yoti_python_sdk.tests.doc_scan.mocks import ( + mocked_request_successful_session_retrieval, +) + +try: + from unittest import mock +except ImportError: + import mock + +SOME_SESSION_ID = "someSessionId" +SOME_MEDIA_ID = "someMediaId" + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_successful_session_creation, +) +def test_should_return_create_session_result(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + :return: + :rtype: + """ + session_spec_mock = mock.Mock(spec=SessionSpec) + session_spec_mock.to_json.return_value = {} + + create_session_result = doc_scan_client.create_session(session_spec_mock) + + assert isinstance(create_session_result, CreateSessionResult) + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_failed_session_creation, +) +def test_should_raise_doc_scan_exception_for_session_creation(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + session_spec_mock = mock.Mock(spec=SessionSpec) + session_spec_mock.to_json.return_value = {} + + with pytest.raises(DocScanException) as ex: + doc_scan_client.create_session(session_spec_mock) + + assert "Failed to create session" in str(ex.value) + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_successful_session_retrieval, +) +def test_should_return_get_session_result(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + session_result = doc_scan_client.get_session(SOME_SESSION_ID) + + assert isinstance(session_result, GetSessionResult) + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_failed_session_retrieval, +) +def test_should_raise_doc_scan_exception_for_session_retrieval(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + with pytest.raises(DocScanException) as ex: + doc_scan_client.get_session(SOME_SESSION_ID) + + doc_scan_exception = ex.value # type: DocScanException + assert "Failed to retrieve session" in str(doc_scan_exception) + assert doc_scan_exception.status_code == 400 + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_server_error, +) +def test_should_raise_exception_for_delete_session(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + with pytest.raises(DocScanException) as ex: + doc_scan_client.delete_session(SOME_SESSION_ID) + + doc_scan_exception = ex.value # type: DocScanException + assert "Failed to delete session" in str(doc_scan_exception) + assert doc_scan_exception.status_code == 500 + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_missing_content, +) +def test_should_raise_exception_for_invalid_content(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + with pytest.raises(DocScanException) as ex: + doc_scan_client.get_media_content(SOME_SESSION_ID, SOME_MEDIA_ID) + + doc_scan_exception = ex.value # type: DocScanException + assert "Failed to retrieve media content" in str(doc_scan_exception) + assert doc_scan_exception.status_code == 404 + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_media_content, +) +def test_should_return_media_value(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + media = doc_scan_client.get_media_content(SOME_SESSION_ID, SOME_MEDIA_ID) + + assert media.mime_type == "application/json" + assert media.content == b"someContent" + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_missing_content, +) +def test_should_throw_exception_for_delete_media(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + with pytest.raises(DocScanException) as ex: + doc_scan_client.delete_media_content(SOME_SESSION_ID, SOME_MEDIA_ID) + + doc_scan_exception = ex.value # type: DocScanException + assert "Failed to delete media content" in str(doc_scan_exception) + assert 404 == doc_scan_exception.status_code diff --git a/yoti_python_sdk/tests/fixtures/response_create_docs_scan_session.txt b/yoti_python_sdk/tests/fixtures/response_create_docs_scan_session.txt new file mode 100644 index 00000000..1059450a --- /dev/null +++ b/yoti_python_sdk/tests/fixtures/response_create_docs_scan_session.txt @@ -0,0 +1,5 @@ +{ + "client_session_token_ttl": 599, + "client_session_token": "", + "session_id": "" +} diff --git a/yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_image.txt b/yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_image.txt new file mode 100644 index 00000000..51319f33 --- /dev/null +++ b/yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_image.txt @@ -0,0 +1 @@ +QUFBQQ== diff --git a/yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_json.txt b/yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_json.txt new file mode 100644 index 00000000..12c75c89 --- /dev/null +++ b/yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_json.txt @@ -0,0 +1,3 @@ +{ + "value": "example" +} diff --git a/yoti_python_sdk/tests/fixtures/response_get_docs_scan_session.txt b/yoti_python_sdk/tests/fixtures/response_get_docs_scan_session.txt new file mode 100644 index 00000000..2bb3194e --- /dev/null +++ b/yoti_python_sdk/tests/fixtures/response_get_docs_scan_session.txt @@ -0,0 +1,196 @@ +{ + "client_session_token_ttl": 599, + "session_id": "", + "user_tracking_id": "", + "state": "COMPLETED", + "client_session_token": "", + "resources": { + "id_documents": [{ + "id": "", + "tasks": [{ + "type": "ID_DOCUMENT_TEXT_DATA_EXTRACTION", + "id": "", + "state": "DONE", + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z", + "generated_checks": [], + "generated_media": [{ + "id": "", + "type": "JSON" + }] + }], + "document_type": "DRIVING_LICENCE", + "issuing_country": "GBR", + "pages": [{ + "capture_method": "CAMERA", + "media": { + "id": "", + "type": "IMAGE", + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + } + }], + "document_fields": { + "media": { + "id": "", + "type": "JSON", + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + } + } + }], + "liveness_capture": [{ + "id": "", + "tasks": [], + "frames": [{ + "media": { + "id": "", + "type": "IMAGE", + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + } + }, + { + "media": { + "id": "", + "type": "IMAGE", + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + } + }, + { + "media": { + "id": "", + "type": "IMAGE", + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + } + }, + {}, + {}, + {}, + {} + ], + "liveness_type": "ZOOM", + "facemap": { + "media": { + "id": "", + "type": "BINARY", + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + } + } + }] + }, + "checks": [{ + "id": "", + "type": "ID_DOCUMENT_AUTHENTICITY", + "state": "DONE", + "resources_used": [ + "" + ], + "report": { + "recommendation": { + "value": "APPROVE" + }, + "breakdown": [{ + "sub_check": "data_in_correct_position", + "result": "PASS", + "details": [] + }, + { + "sub_check": "document_in_date", + "result": "PASS", + "details": [] + }, + { + "sub_check": "expected_data_present", + "result": "PASS", + "details": [] + }, + { + "sub_check": "hologram", + "result": "PASS", + "details": [] + }, + { + "sub_check": "hologram_movement", + "result": "PASS", + "details": [] + }, + { + "sub_check": "no_sign_of_tampering", + "result": "PASS", + "details": [] + }, + { + "sub_check": "other_security_features", + "result": "PASS", + "details": [] + }, + { + "sub_check": "real_document", + "result": "PASS", + "details": [] + } + ] + }, + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + + }, + { + "type": "LIVENESS", + "id": "", + "state": "DONE", + "resources_used": [ + "" + ], + "generated_media": [], + "report": { + "recommendation": { + "value": "APPROVE" + }, + "breakdown": [{ + "sub_check": "liveness_auth", + "result": "PASS", + "details": [] + }] + }, + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + }, + { + "type": "ID_DOCUMENT_FACE_MATCH", + "id": "", + "state": "DONE", + "resources_used": [ + "", + "" + ], + "generated_media": [], + "report": { + "recommendation": { + "value": "APPROVE" + }, + "breakdown": [{ + "sub_check": "manual_face_match", + "result": "PASS", + "details": [] + }, + { + "sub_check": "ai_face_match", + "result": "PASS", + "details": [{ + "name": "confidence_score", + "value": "1.00" + }] + } + + ] + }, + "created": "2020-01-30T15:00:00Z", + "last_updated": "2020-01-30T15:00:00Z" + } + ] +} diff --git a/yoti_python_sdk/tests/mocks.py b/yoti_python_sdk/tests/mocks.py index f2373d07..40cdab6c 100644 --- a/yoti_python_sdk/tests/mocks.py +++ b/yoti_python_sdk/tests/mocks.py @@ -1,11 +1,17 @@ +import base64 from uuid import UUID -from yoti_python_sdk.http import RequestHandler, SignedRequest + +from yoti_python_sdk.http import RequestHandler +from yoti_python_sdk.http import SignedRequest from yoti_python_sdk.http import YotiResponse class MockResponse(YotiResponse): - def __init__(self, status_code, text): - super(MockResponse, self).__init__(status_code, text) + def __init__(self, status_code, text, headers=None, content=None): + if headers is None: + headers = dict() + + super(MockResponse, self).__init__(status_code, text, headers, content) class MockRequestHandler(RequestHandler): @@ -74,3 +80,47 @@ def mocked_requests_post_share_url_invalid_json(*args, **kwargs): def mocked_requests_post_share_url_app_not_found(*args, **kwargs): return MockResponse(status_code=404, text="Application not found") + + +def mocked_request_create_docs_scan_session(*args, **kwargs): + with open( + "yoti_python_sdk/tests/fixtures/response_create_docs_scan_session.txt", "r" + ) as f: + response = f.read() + return MockResponse(status_code=201, text=response) + + +def mocked_request_get_docs_scan_session(*args, **kwargs): + with open( + "yoti_python_sdk/tests/fixtures/response_get_docs_scan_session.txt", "r" + ) as f: + response = f.read() + return MockResponse(status_code=200, text=response) + + +def mocked_request_delete_docs_scan_session(*args, **kwargs): + return MockResponse(status_code=200, text=None) + + +def mocked_request_media_json_retrieval(*args, **kwargs): + with open( + "yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_json.txt", "r" + ) as f: + response = f.read() + return MockResponse( + status_code=200, text=response, headers={"Content-Type": "application/json"} + ) + + +def mocked_request_media_image_retrieval(*args, **kwargs): + with open( + "yoti_python_sdk/tests/fixtures/response_get_docs_scan_media_image.txt", "r" + ) as f: + response = base64.b64decode(f.read()) + return MockResponse( + status_code=200, text=response, headers={"Content-Type": "image/png"} + ) + + +def mocked_request_delete_media(*args, **kwargs): + return MockResponse(status_code=200, text=None) diff --git a/yoti_python_sdk/utils.py b/yoti_python_sdk/utils.py index 773b4112..cd05773d 100644 --- a/yoti_python_sdk/utils.py +++ b/yoti_python_sdk/utils.py @@ -1,5 +1,27 @@ -import uuid import time +import uuid +from abc import ABCMeta +from abc import abstractmethod +from json import JSONEncoder + + +class YotiSerializable(object): + """ + Used to describe a class that is serializable by :class:`YotiEncoder`. + """ + + __metaclass__ = ABCMeta + + @abstractmethod + def to_json(self): + raise NotImplementedError + + +class YotiEncoder(JSONEncoder): + def default(self, o): + if isinstance(o, YotiSerializable): + return o.to_json() + return JSONEncoder.default(self, o) def create_nonce(): diff --git a/yoti_python_sdk/version.py b/yoti_python_sdk/version.py index 0bcaa459..c9b477ce 100644 --- a/yoti_python_sdk/version.py +++ b/yoti_python_sdk/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "2.10.2" +__version__ = "2.11.0"