diff --git a/.travis.yml b/.travis.yml index ab63218b..180a9b1f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,7 +19,7 @@ jobs: - pip install -r requirements.txt - pip install -e .[dev] script: - - py.test -v + - pytest -v - <<: *test python: "3.4" - <<: *test diff --git a/README.md b/README.md index 37900752..9c054599 100644 --- a/README.md +++ b/README.md @@ -34,9 +34,6 @@ How to manage users 1) [Running the examples](#running-the-examples) - How to retrieve a Yoti profile using the token -1) [Running the tests](#running-the-tests) - -Running tests for SDK example - 1) [API Coverage](#api-coverage) - Attributes defined @@ -196,14 +193,6 @@ To run the Flask or Django container: * [Profile - Flask](examples/yoti_example_flask) * [Doc Scan](examples/doc_scan) -## Running the Tests - -Running the tests is done by the following process, ensuring you are using Python 3.0+: - -1. Install dependencies: `pip install -r requirements.txt` -1. Install the SDK and development dependencies: `pip install .[dev]` -1. Execute in the main project dir: `py.test` - ## API Coverage * Activity Details diff --git a/examples/doc_scan/README.md b/examples/doc_scan/README.md index 9bb59371..0ac37ee4 100644 --- a/examples/doc_scan/README.md +++ b/examples/doc_scan/README.md @@ -5,4 +5,4 @@ 1. Rename the [.env.example](.env.example) file to `.env` and fill in the required configuration values 1. Install the dependencies with `pip install -r requirements.txt` 1. Start the server `flask run --cert=adhoc` -1. Visit `https://localhost:5000` \ No newline at end of file +1. Visit `https://localhost:5000` diff --git a/examples/doc_scan/app.py b/examples/doc_scan/app.py index 963d4140..0a862861 100644 --- a/examples/doc_scan/app.py +++ b/examples/doc_scan/app.py @@ -1,19 +1,21 @@ -import base64 -from io import BytesIO - import yoti_python_sdk -from filetype import filetype -from flask import Flask, Response, render_template, request, send_file, session +from flask import Flask, Response, render_template, request, session from yoti_python_sdk.doc_scan import ( DocScanClient, RequestedDocumentAuthenticityCheckBuilder, RequestedFaceMatchCheckBuilder, + RequestedIDDocumentComparisonCheckBuilder, RequestedLivenessCheckBuilder, RequestedTextExtractionTaskBuilder, SdkConfigBuilder, SessionSpecBuilder, ) from yoti_python_sdk.doc_scan.exception import DocScanException +from yoti_python_sdk.doc_scan.session.create.filter import ( + RequiredIdDocumentBuilder, + DocumentRestrictionBuilder, + DocumentRestrictionsFilterBuilder, +) from .settings import YOTI_APP_BASE_URL, YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH @@ -48,7 +50,11 @@ def create_session(): .with_client_session_token_ttl(600) .with_resources_ttl(90000) .with_user_tracking_id("some-user-tracking-id") - .with_requested_check(RequestedDocumentAuthenticityCheckBuilder().build()) + .with_requested_check( + RequestedDocumentAuthenticityCheckBuilder() + .with_manual_check_never() + .build() + ) .with_requested_check( RequestedLivenessCheckBuilder() .for_zoom_liveness() @@ -56,18 +62,37 @@ def create_session(): .build() ) .with_requested_check( - RequestedFaceMatchCheckBuilder().with_manual_check_fallback().build() + RequestedFaceMatchCheckBuilder().with_manual_check_never().build() ) + .with_requested_check(RequestedIDDocumentComparisonCheckBuilder().build()) .with_requested_task( - RequestedTextExtractionTaskBuilder().with_manual_check_always().build() + RequestedTextExtractionTaskBuilder() + .with_manual_check_never() + .with_chip_data_desired() + .build() ) .with_sdk_config(sdk_config) + .with_required_document(build_required_id_document_restriction("PASSPORT")) + .with_required_document( + build_required_id_document_restriction("DRIVING_LICENCE") + ) .build() ) return doc_scan_client.create_session(session_spec) +def build_required_id_document_restriction(document_type): + document_restriction = ( + DocumentRestrictionBuilder().with_document_types([document_type]).build() + ) + + filter_builder = DocumentRestrictionsFilterBuilder().for_whitelist() + filter_builder.with_document_restriction(document_restriction) + + return RequiredIdDocumentBuilder().with_filter(filter_builder.build()).build() + + @app.route("/") def index(): try: @@ -118,8 +143,6 @@ def media(): doc_scan_client = DocScanClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) - base64_req = request.args.get("base64", "0") - session_id = session.get("doc_scan_session_id", None) if session_id is None: return Response("No session ID available", status=404) @@ -129,21 +152,6 @@ def media(): except DocScanException as e: return render_template("error.html", error=e.text) - if base64_req == "1" and retrieved_media.mime_type == "application/octet-stream": - decoded = base64.b64decode(retrieved_media.content) - info = filetype.guess(decoded) - - buffer = BytesIO() - buffer.write(decoded) - buffer.seek(0) - - return send_file( - buffer, - attachment_filename="media." + info.extension, - mimetype=info.mime, - as_attachment=True, - ) - return Response( retrieved_media.content, content_type=retrieved_media.mime_type, status=200 ) diff --git a/examples/doc_scan/templates/success.html b/examples/doc_scan/templates/success.html index ae3842de..646e3c5f 100644 --- a/examples/doc_scan/templates/success.html +++ b/examples/doc_scan/templates/success.html @@ -34,6 +34,12 @@

Get Session Result

User Tracking ID {{ session_result.user_tracking_id }} + {% if session_result.biometric_consent_timestamp is not none %} + + Biometric Consent Timestamp + {{ session_result.biometric_consent_timestamp }} + + {% endif %} @@ -134,6 +140,29 @@

{% endif %} + + {% if session_result.id_document_comparison_checks|length > 0 %} +
+
+

+ +

+
+
+
+ {% for check in session_result.id_document_comparison_checks %} + {% with check=check %} + {% include "partials/check.html" %} + {% endwith %} + {% endfor %} +
+
+
+ {% endif %} @@ -149,7 +178,7 @@

ID Documents

{% with doc_num=0 %} {% for document in session_result.resources.id_documents %} - {% set doc_num = loop.index + 1 %} + {% set doc_num = loop.index %}
@@ -193,6 +222,26 @@
Media
{% endif %} + + {% if document.document_id_photo is not none %} +
+
+

+ +

+
+
+
+ {% if document.document_fields.media is not none %} + + {% endif %} +
+
+
+ {% endif %} + {% if document.text_extraction_tasks|length > 0 %}
@@ -256,36 +305,54 @@
Generated Media
{% endif %} {% if document.pages|length > 0 %} + {% with page_num=0 %} + {% for page in document.pages %} + {% set page_num = loop.index %}
-
+

-
-
-
- {% for page in document.pages %} - {% if page.media is not none %} -
- -
-

Method: {{ page.capture_method }}

-
+
+ +
+ {% if page.media is not none %} +
+ +
+

Method: {{ page.capture_method }}

+
+
+ {% endif %} +
+ + {% if page.frames|length > 0 %} +
+ {% for frame in page.frames %} + {% if frame.media is not none %} +
+ +
+
Frame
- {% endif %} - {% endfor %} -
+
+ {% endif %} + {% endfor %}
+ {% endif %} +
+ {% endfor %} + {% endwith %} {% endif %}
@@ -303,7 +370,7 @@

Zoom Liveness Resources

{% with liveness_num=0 %} {% for liveness in session_result.resources.zoom_liveness_resources %} - {% set liveness_num = loop.index + 1 %} + {% set liveness_num = loop.index %}
@@ -317,39 +384,6 @@

Zoom Liveness Resources

- {% if liveness.facemap is not none %} -
-
-

- -

-
-
-
- {% if liveness.facemap.media is not none %} -

Media

-
- - - - - - -
ID - - {{ liveness.facemap.media.id }} - -
- {% endif %} -
-
-
- {% endif %} - {% if liveness.frames|length > 0 %}
@@ -383,4 +417,4 @@
Frame
{% endfor %} {% endwith %}
-{% include "layout/footer.html" %} \ No newline at end of file +{% include "layout/footer.html" %} diff --git a/examples/yoti_example_django/yoti_example/static/index.css b/examples/yoti_example_django/yoti_example/static/index.css index 979e3575..66bb6b61 100644 --- a/examples/yoti_example_django/yoti_example/static/index.css +++ b/examples/yoti_example_django/yoti_example/static/index.css @@ -26,8 +26,8 @@ font-size: 40px; font-weight: 700; line-height: 1.2; - - margin: 0; + margin-top: 0; + margin-bottom: 80px; text-align: center; @@ -45,8 +45,8 @@ } #yoti-share-button { - width: 130px; - height: 50px; + width: 250px; + height: 45px; } .yoti-login-or-separator { @@ -56,7 +56,7 @@ font-weight: bold; line-height: 1.5; text-align: center; - margin-bottom: 30px; + margin-top: 30px; } .yoti-login-dialog { diff --git a/examples/yoti_example_django/yoti_example/templates/dynamic-share.html b/examples/yoti_example_django/yoti_example/templates/dynamic-share.html index 38799e85..76c7921e 100644 --- a/examples/yoti_example_django/yoti_example/templates/dynamic-share.html +++ b/examples/yoti_example_django/yoti_example/templates/dynamic-share.html @@ -70,8 +70,12 @@

The Yoti app is free to download and use: diff --git a/examples/yoti_example_django/yoti_example/templates/index.html b/examples/yoti_example_django/yoti_example/templates/index.html index 26070ac6..f4ca812d 100644 --- a/examples/yoti_example_django/yoti_example/templates/index.html +++ b/examples/yoti_example_django/yoti_example/templates/index.html @@ -73,8 +73,12 @@

The Yoti app is free to download and use: diff --git a/examples/yoti_example_flask/static/index.css b/examples/yoti_example_flask/static/index.css index 979e3575..66bb6b61 100644 --- a/examples/yoti_example_flask/static/index.css +++ b/examples/yoti_example_flask/static/index.css @@ -26,8 +26,8 @@ font-size: 40px; font-weight: 700; line-height: 1.2; - - margin: 0; + margin-top: 0; + margin-bottom: 80px; text-align: center; @@ -45,8 +45,8 @@ } #yoti-share-button { - width: 130px; - height: 50px; + width: 250px; + height: 45px; } .yoti-login-or-separator { @@ -56,7 +56,7 @@ font-weight: bold; line-height: 1.5; text-align: center; - margin-bottom: 30px; + margin-top: 30px; } .yoti-login-dialog { diff --git a/examples/yoti_example_flask/templates/dynamic-share.html b/examples/yoti_example_flask/templates/dynamic-share.html index 38799e85..76c7921e 100644 --- a/examples/yoti_example_flask/templates/dynamic-share.html +++ b/examples/yoti_example_flask/templates/dynamic-share.html @@ -70,8 +70,12 @@

The Yoti app is free to download and use: diff --git a/examples/yoti_example_flask/templates/index.html b/examples/yoti_example_flask/templates/index.html index 26070ac6..f4ca812d 100644 --- a/examples/yoti_example_flask/templates/index.html +++ b/examples/yoti_example_flask/templates/index.html @@ -73,8 +73,12 @@

The Yoti app is free to download and use: diff --git a/requirements.in b/requirements.in index 2385c225..14edca50 100644 --- a/requirements.in +++ b/requirements.in @@ -1,10 +1,10 @@ asn1==2.2.0 # asn1 2.3.0 introduces enum34 as a dependency, which causes problems on some envs cryptography==2.8.0 -cffi>=1.14.0 +cffi==1.14.3 future==0.18.2 -itsdangerous==0.24 +itsdangerous==1.1.0 pbr==1.10.0 -protobuf==3.12.2 +protobuf==3.13.0 pyopenssl==19.1.0 PyYAML==5.2 # PyYAML 5.3 does not support Python 3.4 pytz==2020.1 @@ -12,4 +12,4 @@ requests>=2.20.0 urllib3>=1.24.3 deprecated==1.2.10 wheel==0.33.6 -iso8601==0.1.12 +iso8601==0.1.13 diff --git a/requirements.txt b/requirements.txt index 99048eea..4a90f75a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -6,16 +6,16 @@ # asn1==2.2.0 # via -r requirements.in certifi==2018.11.29 # via requests -cffi==1.14.0 # via -r requirements.in, cryptography +cffi==1.14.3 # via -r requirements.in, cryptography chardet==3.0.4 # via requests cryptography==2.8 # via -r requirements.in, pyopenssl deprecated==1.2.10 # 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 +iso8601==0.1.13 # via -r requirements.in +itsdangerous==1.1.0 # via -r requirements.in pbr==1.10.0 # via -r requirements.in -protobuf==3.12.2 # via -r requirements.in +protobuf==3.13.0 # via -r requirements.in pycparser==2.18 # via cffi pyopenssl==19.1.0 # via -r requirements.in pytz==2020.1 # via -r requirements.in diff --git a/setup.py b/setup.py index 46d38602..6ea005f6 100644 --- a/setup.py +++ b/setup.py @@ -25,7 +25,7 @@ "future>=0.11.0", "asn1==2.2.0", "pyopenssl>=18.0.0", - "iso8601==0.1.12", + "iso8601==0.1.13", ], extras_require={ "examples": [ @@ -37,14 +37,14 @@ ], "dev": [ "pre-commit==1.17.0", - "pytest>=4.6.0", + "pytest>=4.6.11", "pytest-cov>=2.7.1", "pylint==1.9.4", "pylint-exit>=1.1.0", "python-coveralls==2.9.3", "coverage==4.5.4", "mock==2.0.0", - "virtualenv==20.0.13", + "virtualenv==20.0.33", ], }, classifiers=[ diff --git a/sonar-project.properties b/sonar-project.properties index 543119da..28191bca 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.12.2 +sonar.projectVersion = 2.13.0 sonar.exclusions = yoti_python_sdk/tests/**,examples/**,yoti_python_sdk/protobuf/**/* sonar.python.pylint.reportPath = coverage.out diff --git a/yoti_python_sdk/doc_scan/__init__.py b/yoti_python_sdk/doc_scan/__init__.py index c4abde59..fc9d5733 100644 --- a/yoti_python_sdk/doc_scan/__init__.py +++ b/yoti_python_sdk/doc_scan/__init__.py @@ -1,6 +1,9 @@ from .session.create.check.document_authenticity import ( RequestedDocumentAuthenticityCheckBuilder, ) +from .session.create.check.document_comparison import ( + RequestedIDDocumentComparisonCheckBuilder, +) from .session.create.check.face_match import RequestedFaceMatchCheckBuilder from .session.create.check.liveness import RequestedLivenessCheckBuilder from .session.create.task.text_extraction import RequestedTextExtractionTaskBuilder @@ -10,12 +13,13 @@ from .client import DocScanClient __all__ = [ - RequestedDocumentAuthenticityCheckBuilder, - RequestedLivenessCheckBuilder, - RequestedFaceMatchCheckBuilder, - RequestedTextExtractionTaskBuilder, - SessionSpecBuilder, - NotificationConfigBuilder, - SdkConfigBuilder, - DocScanClient, + "RequestedDocumentAuthenticityCheckBuilder", + "RequestedLivenessCheckBuilder", + "RequestedFaceMatchCheckBuilder", + "RequestedIDDocumentComparisonCheckBuilder", + "RequestedTextExtractionTaskBuilder", + "SessionSpecBuilder", + "NotificationConfigBuilder", + "SdkConfigBuilder", + "DocScanClient", ] diff --git a/yoti_python_sdk/doc_scan/constants.py b/yoti_python_sdk/doc_scan/constants.py index 9cdd96c8..b0f724c3 100644 --- a/yoti_python_sdk/doc_scan/constants.py +++ b/yoti_python_sdk/doc_scan/constants.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals ID_DOCUMENT_AUTHENTICITY = "ID_DOCUMENT_AUTHENTICITY" +ID_DOCUMENT_COMPARISON = "ID_DOCUMENT_COMPARISON" 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" @@ -25,3 +26,6 @@ ALWAYS = "ALWAYS" FALLBACK = "FALLBACK" NEVER = "NEVER" + +DESIRED = "DESIRED" +IGNORE = "IGNORE" diff --git a/yoti_python_sdk/doc_scan/exception/__init__.py b/yoti_python_sdk/doc_scan/exception/__init__.py index aab9483e..b6a4424e 100644 --- a/yoti_python_sdk/doc_scan/exception/__init__.py +++ b/yoti_python_sdk/doc_scan/exception/__init__.py @@ -1,3 +1,3 @@ from .doc_scan_exception import DocScanException -__all__ = [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 index efe0f323..f3e4e26e 100644 --- a/yoti_python_sdk/doc_scan/exception/doc_scan_exception.py +++ b/yoti_python_sdk/doc_scan/exception/doc_scan_exception.py @@ -1,3 +1,6 @@ +import json + + class DocScanException(Exception): """ Exception thrown by the Yoti Doc Scan client @@ -13,7 +16,11 @@ def __init__(self, message, response): """ Exception.__init__(self) - self.__message = message + response_message = self.__get_response_message(response) + + self.__message = message + ( + " - " + response_message if response_message else "" + ) self.__response = response @property @@ -56,5 +63,39 @@ def content(self): """ return self.__response.content + def __get_response_message(self, response): + """ + Return the formatted response message + + :return: the formatted message + :rtype: string or None + """ + if response.headers.get("Content-Type") == "application/json": + return self.__format_json_response_message(json.loads(response.text)) + + return None + + def __format_json_response_message(self, parsed): + """ + Return the formatted JSON response message + + :return: the formatted message + :rtype: string or None + """ + if not parsed.get("code") or not parsed.get("message"): + return None + + code_message = parsed.get("code") + " - " + parsed.get("message") + + errors = [] + for error in parsed.get("errors", []): + if error.get("property") and error.get("message"): + errors.append(error.get("property") + ' "' + error.get("message") + '"') + + if len(errors) > 0: + return code_message + ": " + ", ".join(errors) + + return code_message + def __str__(self): return self.__message diff --git a/yoti_python_sdk/doc_scan/session/create/__init__.py b/yoti_python_sdk/doc_scan/session/create/__init__.py index 7b7896e6..eb0f1857 100644 --- a/yoti_python_sdk/doc_scan/session/create/__init__.py +++ b/yoti_python_sdk/doc_scan/session/create/__init__.py @@ -6,10 +6,10 @@ from .sdk_config import SdkConfigBuilder __all__ = [ - RequestedDocumentAuthenticityCheckBuilder, - RequestedFaceMatchCheckBuilder, - RequestedLivenessCheckBuilder, - NotificationConfigBuilder, - SessionSpecBuilder, - SdkConfigBuilder, + "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 index 3f14e174..c0c38292 100644 --- a/yoti_python_sdk/doc_scan/session/create/check/__init__.py +++ b/yoti_python_sdk/doc_scan/session/create/check/__init__.py @@ -1,9 +1,11 @@ from .document_authenticity import RequestedDocumentAuthenticityCheckBuilder +from .document_comparison import RequestedIDDocumentComparisonCheckBuilder from .face_match import RequestedFaceMatchCheckBuilder from .liveness import RequestedLivenessCheckBuilder __all__ = [ - RequestedDocumentAuthenticityCheckBuilder, - RequestedFaceMatchCheckBuilder, - RequestedLivenessCheckBuilder, + "RequestedDocumentAuthenticityCheckBuilder", + "RequestedIDDocumentComparisonCheckBuilder", + "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 index 032e0a8f..43926725 100644 --- a/yoti_python_sdk/doc_scan/session/create/check/document_authenticity.py +++ b/yoti_python_sdk/doc_scan/session/create/check/document_authenticity.py @@ -1,8 +1,8 @@ # -*- 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 yoti_python_sdk.doc_scan import constants +from yoti_python_sdk.utils import YotiSerializable, remove_null_values from .requested_check import RequestedCheck @@ -11,8 +11,26 @@ class RequestedDocumentAuthenticityCheckConfig(YotiSerializable): The configuration applied when creating a Document Authenticity Check """ + def __init__(self, manual_check=None): + """ + :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 + Authenticity Check + + :return: the manual check value + :rtype: str + """ + return self.__manual_check + def to_json(self): - return {} + return remove_null_values({"manual_check": self.manual_check}) class RequestedDocumentAuthenticityCheck(RequestedCheck): @@ -29,19 +47,50 @@ def __init__(self, config): @property def type(self): - return ID_DOCUMENT_AUTHENTICITY + return constants.ID_DOCUMENT_AUTHENTICITY @property def config(self): return self.__config -class RequestedDocumentAuthenticityCheckBuilder(object): +class RequestedDocumentAuthenticityCheckBuilder: """ Builder to assist creation of :class:`RequestedDocumentAuthenticityCheck` """ - @staticmethod - def build(): - config = RequestedDocumentAuthenticityCheckConfig() + def __init__(self): + self.__manual_check = None + + def with_manual_check_always(self): + """ + :return: the builder + :rtype: RequestedDocumentAuthenticityCheckBuilder + """ + self.__manual_check = constants.ALWAYS + return self + + def with_manual_check_fallback(self): + """ + :return: the builder + :rtype: RequestedDocumentAuthenticityCheckBuilder + """ + self.__manual_check = constants.FALLBACK + return self + + def with_manual_check_never(self): + """ + :return: the builder + :rtype: RequestedDocumentAuthenticityCheckBuilder + """ + self.__manual_check = constants.NEVER + return self + + def build(self=None): + if self is None: + manual_check = None + else: + manual_check = self.__manual_check + + config = RequestedDocumentAuthenticityCheckConfig(manual_check) return RequestedDocumentAuthenticityCheck(config) diff --git a/yoti_python_sdk/doc_scan/session/create/check/document_comparison.py b/yoti_python_sdk/doc_scan/session/create/check/document_comparison.py new file mode 100644 index 00000000..f7dd43fe --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/check/document_comparison.py @@ -0,0 +1,47 @@ +# -*- coding: utf-8 -*- +from __future__ import unicode_literals + +from yoti_python_sdk.doc_scan.constants import ID_DOCUMENT_COMPARISON +from yoti_python_sdk.utils import YotiSerializable +from .requested_check import RequestedCheck + + +class RequestedIDDocumentComparisonCheckConfig(YotiSerializable): + """ + The configuration applied when creating a Document Comparison Check + """ + + def to_json(self): + return {} + + +class RequestedIDDocumentComparisonCheck(RequestedCheck): + """ + Requests creation of a Document Comparison Check + """ + + def __init__(self, config): + """ + :param config: the requested document Comparison check configuration + :type config: RequestedIDDocumentComparisonCheckConfig + """ + self.__config = config + + @property + def type(self): + return ID_DOCUMENT_COMPARISON + + @property + def config(self): + return self.__config + + +class RequestedIDDocumentComparisonCheckBuilder(object): + """ + Builder to assist creation of :class:`RequestedIDDocumentComparisonCheck` + """ + + @staticmethod + def build(): + config = RequestedIDDocumentComparisonCheckConfig() + return RequestedIDDocumentComparisonCheck(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 index 8ae2807f..1abc240e 100644 --- a/yoti_python_sdk/doc_scan/session/create/check/face_match.py +++ b/yoti_python_sdk/doc_scan/session/create/check/face_match.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from yoti_python_sdk.doc_scan import constants -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values from .requested_check import RequestedCheck @@ -30,7 +30,7 @@ def manual_check(self): return self.__manual_check def to_json(self): - return {"manual_check": self.manual_check} + return remove_null_values({"manual_check": self.manual_check}) class RequestedFaceMatchCheck(RequestedCheck): diff --git a/yoti_python_sdk/doc_scan/session/create/check/liveness.py b/yoti_python_sdk/doc_scan/session/create/check/liveness.py index 82cc2fe3..f9dc1075 100644 --- a/yoti_python_sdk/doc_scan/session/create/check/liveness.py +++ b/yoti_python_sdk/doc_scan/session/create/check/liveness.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from yoti_python_sdk.doc_scan import constants -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values from .requested_check import RequestedCheck @@ -40,7 +40,9 @@ def max_retries(self): return self.__max_retries def to_json(self): - return {"liveness_type": self.liveness_type, "max_retries": self.max_retries} + return remove_null_values( + {"liveness_type": self.liveness_type, "max_retries": self.max_retries} + ) class RequestedLivenessCheck(RequestedCheck): 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 index 71a293e0..0b08e399 100644 --- a/yoti_python_sdk/doc_scan/session/create/check/requested_check.py +++ b/yoti_python_sdk/doc_scan/session/create/check/requested_check.py @@ -1,7 +1,7 @@ from abc import ABCMeta from abc import abstractmethod -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values class RequestedCheck(YotiSerializable): @@ -33,4 +33,4 @@ def config(self): raise NotImplementedError def to_json(self): - return {"type": self.type, "config": self.config} + return remove_null_values({"type": self.type, "config": self.config}) diff --git a/yoti_python_sdk/doc_scan/session/create/filter/__init__.py b/yoti_python_sdk/doc_scan/session/create/filter/__init__.py index 222e9083..e1a088e6 100644 --- a/yoti_python_sdk/doc_scan/session/create/filter/__init__.py +++ b/yoti_python_sdk/doc_scan/session/create/filter/__init__.py @@ -6,8 +6,8 @@ from .required_id_document import RequiredIdDocumentBuilder __all__ = [ - DocumentRestrictionsFilterBuilder, - DocumentRestrictionBuilder, - OrthogonalRestrictionsFilterBuilder, - RequiredIdDocumentBuilder, + "DocumentRestrictionsFilterBuilder", + "DocumentRestrictionBuilder", + "OrthogonalRestrictionsFilterBuilder", + "RequiredIdDocumentBuilder", ] diff --git a/yoti_python_sdk/doc_scan/session/create/filter/document_filter.py b/yoti_python_sdk/doc_scan/session/create/filter/document_filter.py index d42066a8..2fd471f9 100644 --- a/yoti_python_sdk/doc_scan/session/create/filter/document_filter.py +++ b/yoti_python_sdk/doc_scan/session/create/filter/document_filter.py @@ -1,6 +1,6 @@ from abc import ABCMeta -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values class DocumentFilter(YotiSerializable): @@ -14,4 +14,4 @@ def type(self): return self.__filter_type def to_json(self): - return {"type": self.type} + return remove_null_values({"type": self.type}) diff --git a/yoti_python_sdk/doc_scan/session/create/filter/document_restrictions_filter.py b/yoti_python_sdk/doc_scan/session/create/filter/document_restrictions_filter.py index 1d597808..427ff8b1 100644 --- a/yoti_python_sdk/doc_scan/session/create/filter/document_restrictions_filter.py +++ b/yoti_python_sdk/doc_scan/session/create/filter/document_restrictions_filter.py @@ -3,7 +3,7 @@ INCLUSION_BLACKLIST, INCLUSION_WHITELIST, ) -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values from .document_filter import DocumentFilter @@ -21,10 +21,12 @@ def document_types(self): return self.__document_types def to_json(self): - return { - "country_codes": self.country_codes, - "document_types": self.document_types, - } + return remove_null_values( + { + "country_codes": self.country_codes, + "document_types": self.document_types, + } + ) class DocumentRestrictionBuilder(object): @@ -85,7 +87,7 @@ def to_json(self): parent = DocumentFilter.to_json(self) parent["inclusion"] = self.inclusion parent["documents"] = self.documents - return parent + return remove_null_values(parent) class DocumentRestrictionsFilterBuilder(object): diff --git a/yoti_python_sdk/doc_scan/session/create/filter/orthogonal_restrictions_filter.py b/yoti_python_sdk/doc_scan/session/create/filter/orthogonal_restrictions_filter.py index 4782cd4a..6e0bcf4e 100644 --- a/yoti_python_sdk/doc_scan/session/create/filter/orthogonal_restrictions_filter.py +++ b/yoti_python_sdk/doc_scan/session/create/filter/orthogonal_restrictions_filter.py @@ -1,7 +1,7 @@ from yoti_python_sdk.doc_scan.constants import INCLUSION_BLACKLIST from yoti_python_sdk.doc_scan.constants import INCLUSION_WHITELIST from yoti_python_sdk.doc_scan.constants import ORTHOGONAL_RESTRICTIONS -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values from .document_filter import DocumentFilter @@ -31,7 +31,9 @@ def country_codes(self): return self.__country_codes def to_json(self): - return {"inclusion": self.inclusion, "country_codes": self.country_codes} + return remove_null_values( + {"inclusion": self.inclusion, "country_codes": self.country_codes} + ) class TypeRestriction(YotiSerializable): @@ -60,7 +62,9 @@ def document_types(self): return self.__document_types def to_json(self): - return {"inclusion": self.inclusion, "document_types": self.document_types} + return remove_null_values( + {"inclusion": self.inclusion, "document_types": self.document_types} + ) class OrthogonalRestrictionsFilter(DocumentFilter): @@ -94,7 +98,7 @@ def to_json(self): parent = DocumentFilter.to_json(self) parent["country_restriction"] = self.country_restriction parent["type_restriction"] = self.type_restriction - return parent + return remove_null_values(parent) class OrthogonalRestrictionsFilterBuilder(object): diff --git a/yoti_python_sdk/doc_scan/session/create/filter/required_id_document.py b/yoti_python_sdk/doc_scan/session/create/filter/required_id_document.py index 3bd3c64d..fe594082 100644 --- a/yoti_python_sdk/doc_scan/session/create/filter/required_id_document.py +++ b/yoti_python_sdk/doc_scan/session/create/filter/required_id_document.py @@ -1,4 +1,5 @@ from yoti_python_sdk.doc_scan.constants import ID_DOCUMENT +from yoti_python_sdk.utils import remove_null_values from .document_filter import DocumentFilter # noqa: F401 from .required_document import RequiredDocument @@ -20,7 +21,7 @@ def filter(self): return self.__doc_filter def to_json(self): - return {"type": self.type, "filter": self.__doc_filter} + return remove_null_values({"type": self.type, "filter": self.__doc_filter}) class RequiredIdDocumentBuilder(object): diff --git a/yoti_python_sdk/doc_scan/session/create/notification_config.py b/yoti_python_sdk/doc_scan/session/create/notification_config.py index 8df275a4..476a38b0 100644 --- a/yoti_python_sdk/doc_scan/session/create/notification_config.py +++ b/yoti_python_sdk/doc_scan/session/create/notification_config.py @@ -5,7 +5,7 @@ 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 +from yoti_python_sdk.utils import YotiSerializable, remove_null_values class NotificationConfig(YotiSerializable): @@ -63,11 +63,13 @@ def topics(self): return self.__topics def to_json(self): - return { - "auth_token": self.auth_token, - "endpoint": self.endpoint, - "topics": self.topics, - } + return remove_null_values( + { + "auth_token": self.auth_token, + "endpoint": self.endpoint, + "topics": self.topics, + } + ) class NotificationConfigBuilder(object): diff --git a/yoti_python_sdk/doc_scan/session/create/sdk_config.py b/yoti_python_sdk/doc_scan/session/create/sdk_config.py index a9246068..c76fe897 100644 --- a/yoti_python_sdk/doc_scan/session/create/sdk_config.py +++ b/yoti_python_sdk/doc_scan/session/create/sdk_config.py @@ -3,12 +3,12 @@ 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 +from yoti_python_sdk.utils import YotiSerializable, remove_null_values class SdkConfig(YotiSerializable): """ - Provides configuration properties using by the web/native clients + Provides configuration properties for the web/native clients """ def __init__( @@ -122,16 +122,18 @@ def error_url(self): 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, - } + return remove_null_values( + { + "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): diff --git a/yoti_python_sdk/doc_scan/session/create/session_spec.py b/yoti_python_sdk/doc_scan/session/create/session_spec.py index 0a7f47c7..ef3bbce4 100644 --- a/yoti_python_sdk/doc_scan/session/create/session_spec.py +++ b/yoti_python_sdk/doc_scan/session/create/session_spec.py @@ -2,7 +2,7 @@ from __future__ import unicode_literals from .filter.required_document import RequiredDocument # noqa: F401 -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values class SessionSpec(YotiSerializable): @@ -20,6 +20,7 @@ def __init__( requested_checks=None, requested_tasks=None, required_documents=None, + block_biometric_consent=None, ): """ :param client_session_token_ttl: the client session token TTL @@ -38,6 +39,8 @@ def __init__( :type requested_tasks: list[RequestedTask] or None :param required_documents: the list of required documents :type required_documents: list[RequiredDocument] or None + :param block_biometric_consent: block the collection of biometric consent + :type block_biometric_consent: bool """ if requested_tasks is None: requested_tasks = [] @@ -54,6 +57,7 @@ def __init__( self.__requested_checks = requested_checks self.__requested_tasks = requested_tasks self.__required_documents = required_documents + self.__block_biometric_consent = block_biometric_consent @property def client_session_token_ttl(self): @@ -78,7 +82,7 @@ def resources_ttl(self): @property def user_tracking_id(self): """ - User tracking ID, for the Relying Business to track returning users + User tracking ID, to track returning users :return: the user tracking ID :rtype: str @@ -138,17 +142,30 @@ def required_documents(self): """ return self.__required_documents + @property + def block_biometric_consent(self): + """ + Whether or not to block the collection of biometric consent. + + :return: block biometric consent + :rtype: bool + """ + return self.__block_biometric_consent + 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, - "required_documents": self.required_documents, - } + return remove_null_values( + { + "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, + "required_documents": self.required_documents, + "block_biometric_consent": self.block_biometric_consent, + } + ) class SessionSpecBuilder(object): @@ -165,6 +182,7 @@ def __init__(self): self.__requested_checks = [] self.__requested_tasks = [] self.__required_documents = [] + self.__block_biometric_consent = None def with_client_session_token_ttl(self, value): """ @@ -262,6 +280,18 @@ def with_required_document(self, required_document): self.__required_documents.append(required_document) return self + def with_block_biometric_consent(self, block_biometric_consent): + """ + Sets whether or not to block the collection of biometric consent + + :param block_biometric_consent: block biometric consent + :type block_biometric_consent: bool + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__block_biometric_consent = block_biometric_consent + return self + def build(self): """ Builds a :class:`SessionSpec` using the supplied values @@ -278,4 +308,5 @@ def build(self): self.__requested_checks, self.__requested_tasks, self.__required_documents, + self.__block_biometric_consent, ) diff --git a/yoti_python_sdk/doc_scan/session/create/task/__init__.py b/yoti_python_sdk/doc_scan/session/create/task/__init__.py index 44755304..6ad73b42 100644 --- a/yoti_python_sdk/doc_scan/session/create/task/__init__.py +++ b/yoti_python_sdk/doc_scan/session/create/task/__init__.py @@ -1,3 +1,3 @@ from .text_extraction import RequestedTextExtractionTaskBuilder -__all__ = [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 index aa67c8e7..110cf07c 100644 --- a/yoti_python_sdk/doc_scan/session/create/task/requested_task.py +++ b/yoti_python_sdk/doc_scan/session/create/task/requested_task.py @@ -1,7 +1,7 @@ from abc import ABCMeta from abc import abstractmethod -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values class RequestedTask(YotiSerializable): @@ -33,4 +33,4 @@ def config(self): raise NotImplementedError def to_json(self): - return {"type": self.type, "config": self.config} + return remove_null_values({"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 index e15939cb..687b41a7 100644 --- a/yoti_python_sdk/doc_scan/session/create/task/text_extraction.py +++ b/yoti_python_sdk/doc_scan/session/create/task/text_extraction.py @@ -2,17 +2,18 @@ from __future__ import unicode_literals from yoti_python_sdk.doc_scan import constants -from yoti_python_sdk.utils import YotiSerializable +from yoti_python_sdk.utils import YotiSerializable, remove_null_values from .requested_task import RequestedTask class RequestedTextExtractionTaskConfig(YotiSerializable): - def __init__(self, manual_check): + def __init__(self, manual_check, chip_data=None): """ :param manual_check: the manual check value :type manual_check: str """ self.__manual_check = manual_check + self.__chip_data = chip_data @property def manual_check(self): @@ -23,8 +24,20 @@ def manual_check(self): """ return self.__manual_check + @property + def chip_data(self): + """ + Describes how to use chip data from an ID document if + it is available + + :return: the chip data usage + """ + return self.__chip_data + def to_json(self): - return {"manual_check": self.manual_check} + return remove_null_values( + {"manual_check": self.manual_check, "chip_data": self.chip_data} + ) class RequestedTextExtractionTask(RequestedTask): @@ -55,6 +68,7 @@ class RequestedTextExtractionTaskBuilder(object): def __init__(self): self.__manual_check = None + self.__chip_data = None def with_manual_check_always(self): """ @@ -86,6 +100,28 @@ def with_manual_check_never(self): self.__manual_check = constants.NEVER return self + def with_chip_data_desired(self): + """ + The TextExtractionTask will use chip data if it is available + + :return: the builder + :rtype: RequestedTextExtractionTaskBuilder + """ + self.__chip_data = constants.DESIRED + return self + + def with_chip_data_ignore(self): + """ + The TextExtractionTask will ignore chip data + + :return: the builder + :rtype: RequestedTextExtractionTaskBuilder + """ + self.__chip_data = constants.IGNORE + return self + def build(self): - config = RequestedTextExtractionTaskConfig(self.__manual_check) + config = RequestedTextExtractionTaskConfig( + self.__manual_check, self.__chip_data + ) return RequestedTextExtractionTask(config) diff --git a/yoti_python_sdk/doc_scan/session/retrieve/check_response.py b/yoti_python_sdk/doc_scan/session/retrieve/check_response.py index c5660ab0..52f78388 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/check_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/check_response.py @@ -148,7 +148,7 @@ class AuthenticityCheckResponse(CheckResponse): class FaceMatchCheckResponse(CheckResponse): """ - Represents a FaceMatch Check for a given session + Represents a FaceMatch check for a given session """ pass @@ -156,7 +156,7 @@ class FaceMatchCheckResponse(CheckResponse): class LivenessCheckResponse(CheckResponse): """ - Represents a Liveness Check for a given session + Represents a Liveness check for a given session """ pass @@ -168,3 +168,11 @@ class TextDataCheckResponse(CheckResponse): """ pass + + +class IDDocumentComparisonCheckResponse(CheckResponse): + """ + Represents an Identity Document Comparison check for a given session + """ + + pass diff --git a/yoti_python_sdk/doc_scan/session/retrieve/document_id_photo_response.py b/yoti_python_sdk/doc_scan/session/retrieve/document_id_photo_response.py new file mode 100644 index 00000000..1c49648e --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/retrieve/document_id_photo_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 DocumentIdPhotoResponse(object): + """ + Represents the document ID photo 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 ID photo + + :return: the media + :rtype: MediaResponse or None + """ + return self.__media 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 index 7405b3e5..71a69a34 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/get_session_result.py @@ -1,10 +1,16 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from iso8601 import ( + ParseError, + iso8601, +) + 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 IDDocumentComparisonCheckResponse from .check_response import LivenessCheckResponse from .check_response import TextDataCheckResponse from .resource_container import ResourceContainer @@ -28,10 +34,32 @@ def __init__(self, data): 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", [])] + self.__biometric_consent_timestamp = self.__parse_date( + data.get("biometric_consent", None) + ) resources = data.get("resources", None) self.__resources = ResourceContainer(resources) or 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 + @staticmethod def __parse_check(check): """ @@ -48,6 +76,7 @@ def __parse_check(check): constants.ID_DOCUMENT_FACE_MATCH: FaceMatchCheckResponse, constants.ID_DOCUMENT_TEXT_DATA_CHECK: TextDataCheckResponse, constants.LIVENESS: LivenessCheckResponse, + constants.ID_DOCUMENT_COMPARISON: IDDocumentComparisonCheckResponse, } clazz = types.get(check.get("type", None), CheckResponse) return clazz(check) @@ -163,6 +192,16 @@ def liveness_checks(self): """ return self.__checks_of_type((LivenessCheckResponse,)) + @property + def id_document_comparison_checks(self): + """ + A filtered list of checks, returning only Identity Document Comparison checks + + :return: the Identity Document Comparison checks + :rtype: list[IDDocumentComparisonCheckResponse] + """ + return self.__checks_of_type((IDDocumentComparisonCheckResponse,)) + @property def resources(self): """ @@ -172,3 +211,13 @@ def resources(self): :rtype: ResourceContainer or None """ return self.__resources + + @property + def biometric_consent_timestamp(self): + """ + The biometric constent timestamp + + :return: the biometric constent timestamp + :rtype: datetime.datetime or None + """ + return self.__biometric_consent_timestamp 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 index 156e14ad..78f28d33 100644 --- 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 @@ -4,6 +4,9 @@ from yoti_python_sdk.doc_scan.session.retrieve.document_fields_response import ( DocumentFieldsResponse, ) +from yoti_python_sdk.doc_scan.session.retrieve.document_id_photo_response import ( + DocumentIdPhotoResponse, +) from yoti_python_sdk.doc_scan.session.retrieve.page_response import PageResponse from yoti_python_sdk.doc_scan.session.retrieve.resource_response import ResourceResponse from yoti_python_sdk.doc_scan.session.retrieve.task_response import ( @@ -34,6 +37,11 @@ def __init__(self, data=None): if "document_fields" in data.keys() else None ) + self.__document_id_photo = ( + DocumentIdPhotoResponse(data["document_id_photo"]) + if "document_id_photo" in data.keys() + else None + ) @property def document_type(self): @@ -75,11 +83,21 @@ def document_fields(self): """ return self.__document_fields + @property + def document_id_photo(self): + """ + Returns the associated document ID photo + + :return: the document ID photo + :rtype: DocumentIdPhotoResponse + """ + return self.__document_id_photo + @property def text_extraction_tasks(self): """ Returns a list of text extraction tasks associated - with the id document + with the identity document :return: list of text extraction tasks :rtype: list[TextExtractionTaskResponse] diff --git a/yoti_python_sdk/doc_scan/session/retrieve/page_response.py b/yoti_python_sdk/doc_scan/session/retrieve/page_response.py index 949ae345..25661660 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/page_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/page_response.py @@ -2,6 +2,7 @@ from __future__ import unicode_literals from .media_response import MediaResponse +from .frame_response import FrameResponse class PageResponse(object): @@ -21,6 +22,7 @@ def __init__(self, data=None): data["capture_method"] if "capture_method" in data.keys() else None ) self.__media = MediaResponse(data["media"]) if "media" in data.keys() else None + self.__frames = [FrameResponse(frame) for frame in data.get("frames", [])] @property def capture_method(self): @@ -41,3 +43,13 @@ def media(self): :rtype: MediaResponse or None """ return self.__media + + @property + def frames(self): + """ + Returns the list of associated frames + + :return: the frames + :rtype: list[FrameResponse] + """ + return self.__frames diff --git a/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py b/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py index f64efcf9..7a54b0bc 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/resource_response.py @@ -4,6 +4,10 @@ class ResourceResponse(object): + """ + Represents a resource + """ + def __init__(self, data=None): """ :param data: the data to parse diff --git a/yoti_python_sdk/doc_scan/support/__init__.py b/yoti_python_sdk/doc_scan/support/__init__.py index eba661aa..a3f18751 100644 --- a/yoti_python_sdk/doc_scan/support/__init__.py +++ b/yoti_python_sdk/doc_scan/support/__init__.py @@ -1,3 +1,3 @@ from .supported_documents import SupportedDocumentsResponse -__all__ = [SupportedDocumentsResponse] +__all__ = ["SupportedDocumentsResponse"] diff --git a/yoti_python_sdk/tests/doc_scan/exception/__init__.py b/yoti_python_sdk/tests/doc_scan/exception/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/doc_scan/exception/test_doc_scan_exception.py b/yoti_python_sdk/tests/doc_scan/exception/test_doc_scan_exception.py new file mode 100644 index 00000000..aad28b36 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/exception/test_doc_scan_exception.py @@ -0,0 +1,90 @@ +import json + +from yoti_python_sdk.doc_scan.exception import DocScanException +from yoti_python_sdk.tests.mocks import MockResponse + + +def test_return_message(): + response = MockResponse(status_code=400, text="some response") + exception = DocScanException("some error", response) + + assert exception.message == "some error" + + +def test_return_only_message_when_html_response(): + response = MockResponse( + status_code=400, + text="some html", + headers={"Content-Type": "text/html"}, + ) + exception = DocScanException("some error", response) + + assert exception.message == "some error" + + +def test_return_only_message_when_json_response_has_no_message_property(): + response = MockResponse( + status_code=400, + text=json.dumps({}), + headers={"Content-Type": "application/json"}, + ) + exception = DocScanException("some error", response) + + assert exception.message == "some error" + + +def test_return_formatted_response_code_and_message(): + response = MockResponse( + status_code=400, + text=json.dumps({"code": "SOME_CODE", "message": "some message"}), + headers={"Content-Type": "application/json"}, + ) + exception = DocScanException("some error", response) + + assert exception.message == "some error - SOME_CODE - some message" + + +def test_return_formatted_response_code_message_and_errors(): + response = MockResponse( + status_code=400, + text=json.dumps( + { + "code": "SOME_CODE", + "message": "some message", + "errors": [ + {"property": "some.property", "message": "some message"}, + { + "property": "some.other.property", + "message": "some other message", + }, + ], + } + ), + headers={"Content-Type": "application/json"}, + ) + exception = DocScanException("some error", response) + + assert ( + exception.message + == 'some error - SOME_CODE - some message: some.property "some message", some.other.property "some other message"' + ) + + +def test_excludes_errors_without_property_or_message(): + response = MockResponse( + status_code=400, + text=json.dumps( + { + "code": "SOME_CODE", + "message": "some message", + "errors": [ + {"message": "some message"}, + {"property": "some.other.property"}, + ], + } + ), + headers={"Content-Type": "application/json"}, + ) + exception = DocScanException("some error", response) + + assert exception.message == "some error - SOME_CODE - some message" 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 index ae0d0596..404f0bfe 100644 --- 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 @@ -29,6 +29,33 @@ def test_should_serialize_to_json_without_error(self): s = json.dumps(result, cls=YotiEncoder) assert s is not None and s != "" + def test_should_build_with_manual_check_always(self): + result = ( + RequestedDocumentAuthenticityCheckBuilder() + .with_manual_check_always() + .build() + ) + + assert result.config.manual_check == "ALWAYS" + + def test_should_build_with_manual_check_fallback(self): + result = ( + RequestedDocumentAuthenticityCheckBuilder() + .with_manual_check_fallback() + .build() + ) + + assert result.config.manual_check == "FALLBACK" + + def test_should_build_with_manual_check_never(self): + result = ( + RequestedDocumentAuthenticityCheckBuilder() + .with_manual_check_never() + .build() + ) + + assert result.config.manual_check == "NEVER" + if __name__ == "__main__": unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/create/check/test_requested_document_comparison_check.py b/yoti_python_sdk/tests/doc_scan/session/create/check/test_requested_document_comparison_check.py new file mode 100644 index 00000000..4b853bcf --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/check/test_requested_document_comparison_check.py @@ -0,0 +1,33 @@ +import json +import unittest + +from yoti_python_sdk.doc_scan.session.create.check import ( + RequestedIDDocumentComparisonCheckBuilder, +) +from yoti_python_sdk.doc_scan.session.create.check.document_comparison import ( + RequestedIDDocumentComparisonCheck, +) +from yoti_python_sdk.doc_scan.session.create.check.document_comparison import ( + RequestedIDDocumentComparisonCheckConfig, +) +from yoti_python_sdk.doc_scan.session.create.check.document_comparison import ( + RequestedIDDocumentComparisonCheckBuilder, +) +from yoti_python_sdk.doc_scan.session.create.check.requested_check import RequestedCheck +from yoti_python_sdk.utils import YotiEncoder + + +def test_should_build_correctly(): + result = RequestedIDDocumentComparisonCheckBuilder().build() + + assert isinstance(result, RequestedCheck) + assert isinstance(result, RequestedIDDocumentComparisonCheck) + assert isinstance(result.config, RequestedIDDocumentComparisonCheckConfig) + assert result.type == "ID_DOCUMENT_COMPARISON" + + +def test_should_serialize_to_json_without_error(): + result = RequestedIDDocumentComparisonCheckBuilder().build() + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None and s != "" diff --git a/yoti_python_sdk/tests/doc_scan/session/create/filter/test_document_restriction_builder.py b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_document_restriction_builder.py index d761827d..6ca116ce 100644 --- a/yoti_python_sdk/tests/doc_scan/session/create/filter/test_document_restriction_builder.py +++ b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_document_restriction_builder.py @@ -41,3 +41,15 @@ def test_to_json_should_not_throw_exception(): s = json.dumps(result, cls=YotiEncoder) assert s is not None and s != "" + + +def test_to_json_should_not_include_null_values(): + result = ( + DocumentRestrictionBuilder() + .with_document_types(["PASSPORT", "DRIVING_LICENCE"]) + .build() + ) + + s = json.dumps(result, cls=YotiEncoder) + assert s is not None + assert "null" not in s 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 index 2d20ae47..79806520 100644 --- 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 @@ -15,14 +15,18 @@ class RequestedTextExtractionTaskTest(unittest.TestCase): - def test_should_build_with_manual_check_always(self): - result = RequestedTextExtractionTaskBuilder().with_manual_check_always().build() + def test_should_build_text_data_extraction_task(self): + result = RequestedTextExtractionTaskBuilder().build() assert isinstance(result, RequestedTask) assert isinstance(result, RequestedTextExtractionTask) assert isinstance(result.config, RequestedTextExtractionTaskConfig) assert result.type == "ID_DOCUMENT_TEXT_DATA_EXTRACTION" + + def test_should_build_with_manual_check_always(self): + result = RequestedTextExtractionTaskBuilder().with_manual_check_always().build() + assert result.config.manual_check == "ALWAYS" def test_should_build_with_manual_check_fallback(self): @@ -37,12 +41,46 @@ def test_should_build_with_manual_check_never(self): assert result.config.manual_check == "NEVER" + def test_should_build_with_chip_data_desired(self): + result = RequestedTextExtractionTaskBuilder().with_chip_data_desired().build() + + assert result.config.chip_data == "DESIRED" + + def test_should_build_with_chip_data_ignore(self): + result = RequestedTextExtractionTaskBuilder().with_chip_data_ignore().build() + + assert result.config.chip_data == "IGNORE" + 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 != "" + def test_to_json_should_return_correct_properties(self): + result = ( + RequestedTextExtractionTaskBuilder() + .with_manual_check_always() + .with_chip_data_desired() + .build() + ) + + json = result.to_json() + assert json is not None + + json_config = json.get("config").to_json() + assert json_config.get("manual_check") == "ALWAYS" + assert json_config.get("chip_data") == "DESIRED" + + def test_to_json_should_not_include_null_config_values(self): + result = RequestedTextExtractionTaskBuilder().build() + + json = result.to_json() + assert json is not None + + json_config = json.get("config").to_json() + assert json_config == {} + 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 index 9130a8ca..41f0a5e5 100644 --- 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 @@ -104,6 +104,21 @@ def test_should_default_empty_arrays(self): assert len(result.requested_tasks) == 0 assert len(result.required_documents) == 0 + def test_should_build_correctly_with_block_biometric_consent_true(self): + result = SessionSpecBuilder().with_block_biometric_consent(True).build() + + assert result.block_biometric_consent is True + + def test_should_build_correctly_with_block_biometric_consent_false(self): + result = SessionSpecBuilder().with_block_biometric_consent(False).build() + + assert result.block_biometric_consent is False + + def test_should_build_correctly_without_block_biometric_consent_false(self): + result = SessionSpecBuilder().build() + + assert result.block_biometric_consent is None + if __name__ == "__main__": unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_document_id_photo_response.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_document_id_photo_response.py new file mode 100644 index 00000000..3dd31f04 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_document_id_photo_response.py @@ -0,0 +1,22 @@ +import unittest + +from yoti_python_sdk.doc_scan.session.retrieve.document_id_photo_response import ( + DocumentIdPhotoResponse, +) +from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse + + +class DocumentIdPhotoResponseTest(unittest.TestCase): + def test_should_parse_correctly(self): + data = {"media": {}} + + result = DocumentIdPhotoResponse(data) + assert isinstance(result.media, MediaResponse) + + def test_should_not_throw_exception_for_none(self): + result = DocumentIdPhotoResponse(None) + assert result.media 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 index 0b259771..b1634876 100644 --- 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 @@ -1,5 +1,9 @@ import unittest +from datetime import datetime + +import pytz + from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( AuthenticityCheckResponse, ) @@ -9,6 +13,9 @@ from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( LivenessCheckResponse, ) +from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( + IDDocumentComparisonCheckResponse, +) from yoti_python_sdk.doc_scan.session.retrieve.check_response import ( TextDataCheckResponse, ) @@ -26,13 +33,26 @@ class GetSessionResultTest(unittest.TestCase): SOME_USER_TRACKING_ID = "someUserTrackingId" SOME_STATE = "someState" SOME_CLIENT_SESSION_TOKEN = "someClientSessionToken" + SOME_BIOMETRIC_CONSENT = "2019-05-01T05:01:48.000Z" SOME_CHECKS = [ {"type": "ID_DOCUMENT_AUTHENTICITY"}, {"type": "ID_DOCUMENT_TEXT_DATA_CHECK"}, {"type": "ID_DOCUMENT_FACE_MATCH"}, {"type": "LIVENESS"}, + {"type": "ID_DOCUMENT_COMPARISON"}, ] + EXPECTED_BIOMETRIC_CONSENT_DATETIME = datetime( + year=2019, + month=5, + day=1, + hour=5, + minute=1, + second=48, + microsecond=0, + tzinfo=pytz.utc, + ) + def test_should_parse_different_checks(self): data = { "client_session_token_ttl": self.SOME_CLIENT_SESSION_TOKEN_TTL, @@ -42,6 +62,7 @@ def test_should_parse_different_checks(self): "user_tracking_id": self.SOME_USER_TRACKING_ID, "checks": self.SOME_CHECKS, "resources": {}, + "biometric_consent": self.SOME_BIOMETRIC_CONSENT, } result = GetSessionResult(data) @@ -52,24 +73,32 @@ def test_should_parse_different_checks(self): assert result.state is self.SOME_STATE assert result.user_tracking_id is self.SOME_USER_TRACKING_ID - assert len(result.checks) == 4 + assert len(result.checks) == 5 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.checks[4], IDDocumentComparisonCheckResponse) assert isinstance(result.resources, ResourceContainer) + assert isinstance(result.biometric_consent_timestamp, datetime) + assert ( + result.biometric_consent_timestamp + == self.EXPECTED_BIOMETRIC_CONSENT_DATETIME + ) + def test_should_filter_checks(self): data = {"checks": self.SOME_CHECKS} result = GetSessionResult(data) - assert len(result.checks) == 4 + assert len(result.checks) == 5 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 + assert len(result.id_document_comparison_checks) == 1 if __name__ == "__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 index deeac750..6c9686c1 100644 --- 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 @@ -3,6 +3,9 @@ from yoti_python_sdk.doc_scan.session.retrieve.document_fields_response import ( DocumentFieldsResponse, ) +from yoti_python_sdk.doc_scan.session.retrieve.document_id_photo_response import ( + DocumentIdPhotoResponse, +) from yoti_python_sdk.doc_scan.session.retrieve.id_document_resource_response import ( IdDocumentResourceResponse, ) @@ -22,6 +25,7 @@ class IdDocumentResourceResponseTest(unittest.TestCase): ] SOME_PAGES = [{"first": "page"}, {"second": "page"}] SOME_DOCUMENT_FIELDS = {"media": {}} + SOME_DOCUMENT_ID_PHOTO = {"media": {}} def test_should_parse_correctly(self): data = { @@ -31,6 +35,7 @@ def test_should_parse_correctly(self): "tasks": self.SOME_TASKS, "pages": self.SOME_PAGES, "document_fields": self.SOME_DOCUMENT_FIELDS, + "document_id_photo": self.SOME_DOCUMENT_ID_PHOTO, } result = IdDocumentResourceResponse(data) @@ -41,6 +46,7 @@ def test_should_parse_correctly(self): assert len(result.tasks) == 2 assert len(result.pages) == 2 assert isinstance(result.document_fields, DocumentFieldsResponse) + assert isinstance(result.document_id_photo, DocumentIdPhotoResponse) def test_should_parse_when_none(self): result = IdDocumentResourceResponse(None) @@ -51,6 +57,7 @@ def test_should_parse_when_none(self): assert len(result.tasks) == 0 assert len(result.pages) == 0 assert result.document_fields is None + assert result.document_id_photo is None def test_should_parse_tasks_with_type(self): data = { 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 index 07214d3e..22e66d18 100644 --- 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 @@ -2,24 +2,34 @@ from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse from yoti_python_sdk.doc_scan.session.retrieve.page_response import PageResponse +from yoti_python_sdk.doc_scan.session.retrieve.frame_response import FrameResponse class PageResponseTest(unittest.TestCase): SOME_CAPTURE_METHOD = "someCaptureMethod" + SOME_FRAMES = [{"first": "frame"}, {"second": "frame"}] def test_should_parse_correctly(self): - data = {"capture_method": self.SOME_CAPTURE_METHOD, "media": {}} + data = { + "capture_method": self.SOME_CAPTURE_METHOD, + "media": {}, + "frames": self.SOME_FRAMES, + } result = PageResponse(data) assert result.capture_method is self.SOME_CAPTURE_METHOD assert isinstance(result.media, MediaResponse) + assert len(result.frames) == 2 + assert isinstance(result.frames[0], FrameResponse) + assert isinstance(result.frames[1], FrameResponse) def test_should_parse_with_none(self): result = PageResponse(None) assert result.capture_method is None assert result.media is None + assert len(result.frames) == 0 if __name__ == "__main__": diff --git a/yoti_python_sdk/tests/mocks.py b/yoti_python_sdk/tests/mocks.py index 40cdab6c..a1981157 100644 --- a/yoti_python_sdk/tests/mocks.py +++ b/yoti_python_sdk/tests/mocks.py @@ -1,5 +1,6 @@ import base64 from uuid import UUID +from requests.structures import CaseInsensitiveDict from yoti_python_sdk.http import RequestHandler from yoti_python_sdk.http import SignedRequest @@ -11,7 +12,9 @@ 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) + super(MockResponse, self).__init__( + status_code, text, CaseInsensitiveDict(headers), content + ) class MockRequestHandler(RequestHandler): diff --git a/yoti_python_sdk/utils.py b/yoti_python_sdk/utils.py index cd05773d..110fb8c6 100644 --- a/yoti_python_sdk/utils.py +++ b/yoti_python_sdk/utils.py @@ -40,3 +40,15 @@ def create_timestamp(): :return: the timestamp as a int """ return int(time.time() * 1000) + + +def remove_null_values(d): + """ + Delete keys with the value ``None`` in a dictionary, recursively. (None serializes to null) + """ + for key, value in list(d.items()): + if value is None: + del d[key] + elif isinstance(value, dict): + remove_null_values(value) + return d diff --git a/yoti_python_sdk/version.py b/yoti_python_sdk/version.py index 953079f5..1947b0b3 100644 --- a/yoti_python_sdk/version.py +++ b/yoti_python_sdk/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "2.12.2" +__version__ = "2.13.0"