diff --git a/README.md b/README.md index 44f1ec85..fc7a206f 100644 --- a/README.md +++ b/README.md @@ -247,31 +247,12 @@ To run the Flask or Django container: ### Running Locally -1. Both example applications utilise the env variables described in [Configuration](#configuration), make sure they are accessible. -1. Ensure pip is up to date: `easy_install --upgrade pip` -1. Ensure setuptools and wheel are up to date: `python -m pip install --upgrade setuptools wheel` +#### Follow instructions in the README for each example: -#### Flask - -1. Change directories to the Flask project: `cd examples/yoti_example_flask` -1. Install dependencies: `pip install -r requirements.txt` -1. Run `python app.py` -1. Navigate to https://localhost:5000 - -#### Django - -1. You will need Python 3+ to run the Django example -1. Change directories to the Django project: `cd examples/yoti_example_django` -1. Install dependencies: `pip install -r requirements.txt` -1. Apply migrations before the first start by running: `python manage.py migrate` -1. Run: `python manage.py runsslserver 0.0.0.0:5000` -1. Navigate to https://localhost:5000 - -#### AML Example - -1. Change directories to the AML folder: `cd examples/aml` -1. Install requirements with `pip install -r requirements.txt` -1. Run: `python app.py` +* [Profile - Django](examples/yoti_example_django) +* [Profile - Flask](examples/yoti_example_flask) +* [AML](examples/aml) +* [Doc Scan](examples/doc_scan) ## Running the Tests diff --git a/examples/aml/README.md b/examples/aml/README.md new file mode 100644 index 00000000..b5bafaa3 --- /dev/null +++ b/examples/aml/README.md @@ -0,0 +1,5 @@ +### AML Example Project + +1. Rename the [.env.example](.env.example) file to `.env` and fill in the required configuration values +1. Install requirements with `pip install -r requirements.txt` +1. Run: `python app.py` \ No newline at end of file diff --git a/examples/doc_scan/.env.example b/examples/doc_scan/.env.example new file mode 100644 index 00000000..950e65a9 --- /dev/null +++ b/examples/doc_scan/.env.example @@ -0,0 +1,3 @@ +# Required Keys +YOTI_CLIENT_SDK_ID=yourClientSdkId +YOTI_KEY_FILE_PATH=yourKeyFilePath diff --git a/examples/doc_scan/.gitignore b/examples/doc_scan/.gitignore new file mode 100644 index 00000000..612424a3 --- /dev/null +++ b/examples/doc_scan/.gitignore @@ -0,0 +1 @@ +*.pem \ No newline at end of file diff --git a/examples/doc_scan/README.md b/examples/doc_scan/README.md new file mode 100644 index 00000000..9bb59371 --- /dev/null +++ b/examples/doc_scan/README.md @@ -0,0 +1,8 @@ +# Doc Scan Example + +## Running the example + +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 diff --git a/examples/doc_scan/__init__.py b/examples/doc_scan/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/doc_scan/app.py b/examples/doc_scan/app.py new file mode 100644 index 00000000..963d4140 --- /dev/null +++ b/examples/doc_scan/app.py @@ -0,0 +1,153 @@ +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 yoti_python_sdk.doc_scan import ( + DocScanClient, + RequestedDocumentAuthenticityCheckBuilder, + RequestedFaceMatchCheckBuilder, + RequestedLivenessCheckBuilder, + RequestedTextExtractionTaskBuilder, + SdkConfigBuilder, + SessionSpecBuilder, +) +from yoti_python_sdk.doc_scan.exception import DocScanException + +from .settings import YOTI_APP_BASE_URL, YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH + +app = Flask(__name__) +app.secret_key = "someSecretKey" + + +def create_session(): + """ + Creates a Doc Scan session + + :return: the create session result + :rtype: CreateSessionResult + """ + doc_scan_client = DocScanClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) + + sdk_config = ( + SdkConfigBuilder() + .with_allows_camera_and_upload() + .with_primary_colour("#2d9fff") + .with_secondary_colour("#FFFFFF") + .with_font_colour("#FFFFFF") + .with_locale("en-GB") + .with_preset_issuing_country("GBR") + .with_success_url("{url}/success".format(url=YOTI_APP_BASE_URL)) + .with_error_url("{url}/error".format(url=YOTI_APP_BASE_URL)) + .build() + ) + + session_spec = ( + SessionSpecBuilder() + .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( + RequestedLivenessCheckBuilder() + .for_zoom_liveness() + .with_max_retries(1) + .build() + ) + .with_requested_check( + RequestedFaceMatchCheckBuilder().with_manual_check_fallback().build() + ) + .with_requested_task( + RequestedTextExtractionTaskBuilder().with_manual_check_always().build() + ) + .with_sdk_config(sdk_config) + .build() + ) + + return doc_scan_client.create_session(session_spec) + + +@app.route("/") +def index(): + try: + result = create_session() + except DocScanException as e: + return render_template("error.html", error=e.text) + + session["doc_scan_session_id"] = result.session_id + + iframe_url = "{base_url}/web/index.html?sessionID={session_id}&sessionToken={session_token}".format( + base_url=yoti_python_sdk.YOTI_DOC_SCAN_API_URL, + session_id=result.session_id, + session_token=result.client_session_token, + ) + + return render_template("index.html", iframe_url=iframe_url) + + +@app.route("/success") +def success(): + doc_scan_client = DocScanClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH) + + session_id = session.get("doc_scan_session_id", None) + + try: + session_result = doc_scan_client.get_session(session_id) + except DocScanException as e: + return render_template("error.html", error=e.text) + + return render_template("success.html", session_result=session_result) + + +@app.route("/error") +def error(): + error_message = "An unknown error occurred" + + if request.args.get("yotiErrorCode", None) is not None: + error_message = "Error Code: {}".format(request.args.get("yotiErrorCode")) + + return render_template("error.html", error=error_message) + + +@app.route("/media") +def media(): + media_id = request.args.get("mediaId", None) + if media_id is None: + return Response(status=404) + + 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) + + try: + retrieved_media = doc_scan_client.get_media_content(session_id, media_id) + 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 + ) + + +if __name__ == "__main__": + app.run() diff --git a/examples/doc_scan/requirements.in b/examples/doc_scan/requirements.in new file mode 100644 index 00000000..d42ad858 --- /dev/null +++ b/examples/doc_scan/requirements.in @@ -0,0 +1,5 @@ +flask>=1.1.2 +python-dotenv>=0.13.0 +yoti>=2.11.2 +filetype>=1.0.7 +pyopenssl>=19.1.0 \ No newline at end of file diff --git a/examples/doc_scan/requirements.txt b/examples/doc_scan/requirements.txt new file mode 100644 index 00000000..052d6cf6 --- /dev/null +++ b/examples/doc_scan/requirements.txt @@ -0,0 +1,34 @@ +# +# This file is autogenerated by pip-compile +# To update, run: +# +# pip-compile --output-file=requirements.txt requirements.in +# +asn1==2.2.0 # via yoti +certifi==2020.4.5.1 # via requests +cffi==1.14.0 # via cryptography +chardet==3.0.4 # via requests +click==7.1.2 # via flask +cryptography==2.9.2 # via pyopenssl, yoti +deprecated==1.2.6 # via yoti +filetype==1.0.7 # via -r requirements.in +flask==1.1.2 # via -r requirements.in +future==0.18.2 # via yoti +idna==2.9 # via requests +iso8601==0.1.12 # via yoti +itsdangerous==1.1.0 # via flask +jinja2==2.11.2 # via flask +markupsafe==1.1.1 # via jinja2 +protobuf==3.11.3 # via yoti +pycparser==2.20 # via cffi +pyopenssl==19.1.0 # via -r requirements.in, yoti +python-dotenv==0.13.0 # via -r requirements.in +requests==2.23.0 # via yoti +six==1.14.0 # via cryptography, protobuf, pyopenssl +urllib3==1.25.9 # via requests +werkzeug==1.0.1 # via flask +wrapt==1.12.1 # via deprecated +yoti==2.11.2 # via -r requirements.in + +# The following packages are considered to be unsafe in a requirements file: +# setuptools diff --git a/examples/doc_scan/settings.py b/examples/doc_scan/settings.py new file mode 100644 index 00000000..57f78723 --- /dev/null +++ b/examples/doc_scan/settings.py @@ -0,0 +1,15 @@ +from os import environ +from os.path import dirname, join + +from dotenv import load_dotenv + +dotenv_path = join(dirname(__file__), ".env") +load_dotenv(dotenv_path) + +YOTI_CLIENT_SDK_ID = environ.get("YOTI_CLIENT_SDK_ID", None) +YOTI_KEY_FILE_PATH = environ.get("YOTI_KEY_FILE_PATH", None) + +if YOTI_CLIENT_SDK_ID is None or YOTI_KEY_FILE_PATH is None: + raise ValueError("YOTI_CLIENT_SDK_ID or YOTI_KEY_FILE_PATH is None") + +YOTI_APP_BASE_URL = environ.get("YOTI_APP_BASE_URL", "https://localhost:5000") diff --git a/examples/doc_scan/static/images/favicon.png b/examples/doc_scan/static/images/favicon.png new file mode 100644 index 00000000..7dc99bcd Binary files /dev/null and b/examples/doc_scan/static/images/favicon.png differ diff --git a/examples/doc_scan/static/images/logo.svg b/examples/doc_scan/static/images/logo.svg new file mode 100644 index 00000000..425e32c5 --- /dev/null +++ b/examples/doc_scan/static/images/logo.svg @@ -0,0 +1 @@ +yoti_logos_RGB \ No newline at end of file diff --git a/examples/doc_scan/static/style.css b/examples/doc_scan/static/style.css new file mode 100644 index 00000000..8ae73f4b --- /dev/null +++ b/examples/doc_scan/static/style.css @@ -0,0 +1,8 @@ + +body { + padding-top: 4.5rem; +} + +table td:first-child { + width: 30%; +} \ No newline at end of file diff --git a/examples/doc_scan/templates/error.html b/examples/doc_scan/templates/error.html new file mode 100644 index 00000000..0070f8e7 --- /dev/null +++ b/examples/doc_scan/templates/error.html @@ -0,0 +1,9 @@ +{% include "layout/header.html" %} +
+
+
+

{{ error }}

+
+
+
+{% include "layout/footer.html" %} \ No newline at end of file diff --git a/examples/doc_scan/templates/index.html b/examples/doc_scan/templates/index.html new file mode 100644 index 00000000..09d8d1f7 --- /dev/null +++ b/examples/doc_scan/templates/index.html @@ -0,0 +1,3 @@ +{% include "layout/header.html" %} + +{% include "layout/footer.html" %} \ No newline at end of file diff --git a/examples/doc_scan/templates/layout/footer.html b/examples/doc_scan/templates/layout/footer.html new file mode 100644 index 00000000..ea33a259 --- /dev/null +++ b/examples/doc_scan/templates/layout/footer.html @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/examples/doc_scan/templates/layout/header.html b/examples/doc_scan/templates/layout/header.html new file mode 100644 index 00000000..8b2b357d --- /dev/null +++ b/examples/doc_scan/templates/layout/header.html @@ -0,0 +1,18 @@ + + + + + Yoti Doc Scan + + + + + + + \ No newline at end of file diff --git a/examples/doc_scan/templates/partials/check.html b/examples/doc_scan/templates/partials/check.html new file mode 100644 index 00000000..251f834a --- /dev/null +++ b/examples/doc_scan/templates/partials/check.html @@ -0,0 +1,115 @@ + + + + + + + + + + + + + + + + + + + + + + + + + {% if check.report is not none %} + {% if check.report.recommendation is not none %} + + + + + {% endif %} + {% if check.report.breakdown|length > 0 %} + + + + + {% endif %} + {% endif %} + + {% if check.generated_media|length > 0 %} + + + + + {% endif %} + +
ID{{ check.id }}
State + + {{ check.state }} + +
Created{{ check.created }}
Last Updated{{ check.last_updated }}
Resources Used{{ check.resources_used|join(", ") }}
Recommendation + + + + + + + + + + + + + + + +
Value{{ check.report.recommendation.value }}
Reason{{ check.report.recommendation.reason }}
Recovery Suggestion{{ check.report.recommendation.recovery_suggestion }}
+
Breakdown + {% for breakdown in check.report.breakdown %} + + + + + + + + + + + {% if breakdown.details|length > 0 %} + + + + + {% endif %} + +
Sub Check{{ breakdown.sub_check }}
Result{{ breakdown.result }}
Details + + + {% for detail in breakdown.details %} + + + + + {% endfor %} + +
{{ detail.name }}{{ detail.value }}
+
+ {% endfor %} +
Generated Media + {% for media in check.generated_media %} + + + + + + + + + + + +
ID{{ media.id }}
Type{{ media.type }}
+ {% endfor %} +
\ No newline at end of file diff --git a/examples/doc_scan/templates/partials/task.html b/examples/doc_scan/templates/partials/task.html new file mode 100644 index 00000000..b52754e5 --- /dev/null +++ b/examples/doc_scan/templates/partials/task.html @@ -0,0 +1,24 @@ + + + + + + + + + + + + + + + + + + + +
ID{{ task.id }}
State + + {{ task.state }} + +
Created{{ task.created }}
Last Updated{{ task.last_updated }}
\ No newline at end of file diff --git a/examples/doc_scan/templates/success.html b/examples/doc_scan/templates/success.html new file mode 100644 index 00000000..ae3842de --- /dev/null +++ b/examples/doc_scan/templates/success.html @@ -0,0 +1,386 @@ +{% include "layout/header.html" %} +
+
+
+

Get Session Result

+ + + + + + + + + + + + {% if session_result.client_session_token is not none %} + + + + + {% endif %} + + + + + + + + + +
Session ID{{ session_result.session_id }}
State + + {{ session_result.state }} + +
Client Session Token + {{ session_result.client_session_token }} +
Client Session Token TTL{{ session_result.client_session_token_ttl }}
User Tracking ID{{ session_result.user_tracking_id }}
+
+
+ + {% if session_result.checks|length > 0 %} +
+
+

Checks

+
+ {% if session_result.authenticity_checks|length > 0 %} +
+
+

+ +

+
+
+
+ {% for check in session_result.authenticity_checks %} + {% with check=check %} + {% include "partials/check.html" %} + {% endwith %} + {% endfor %} +
+
+
+ {% endif %} + {% if session_result.text_data_checks|length > 0 %} +
+
+

+ +

+
+
+
+ {% for check in session_result.text_data_checks %} + {% with check=check %} + {% include "partials/check.html" %} + {% endwith %} + {% endfor %} +
+
+
+ {% endif %} + {% if session_result.face_match_checks|length > 0 %} +
+
+

+ +

+
+
+
+ {% for check in session_result.face_match_checks %} + {% with check=check %} + {% include "partials/check.html" %} + {% endwith %} + {% endfor %} +
+
+
+ {% endif %} + {% if session_result.liveness_checks|length > 0 %} +
+
+

+ +

+
+
+
+ {% for check in session_result.authenticity_checks %} + {% with check=check %} + {% include "partials/check.html" %} + {% endwith %} + {% endfor %} +
+
+
+ {% endif %} +
+
+
+ {% endif %} + + {% if session_result.resources.id_documents|length > 0 %} +
+
+

ID Documents

+
+
+ {% endif %} + + {% with doc_num=0 %} + {% for document in session_result.resources.id_documents %} + {% set doc_num = loop.index + 1 %} +
+
+ +

+ {{ document.document_type }} {{ document.issuing_country }} +

+ +
+ {% if document.document_fields is not none %} +
+
+

+ +

+
+
+
+ {% if document.document_fields.media is not none %} +
Media
+ + + + + + + +
ID + + {{ document.document_fields.media.id }} + +
+ {% endif %} +
+
+
+ {% endif %} + {% if document.text_extraction_tasks|length > 0 %} +
+
+

+ +

+
+
+
+ {% for task in document.text_extraction_tasks %} + + {% with task=task %} + {% include "partials/task.html" %} + {% endwith %} + + {% if task.generated_text_data_checks|length > 0 %} +
Generated Text Data Checks
+ + {% for generated_check in task.generated_text_data_checks %} + + + + + + + +
ID{{ generated_check.id }}
+ {% endfor %} + {% endif %} + + {% if task.generated_media|length > 0 %} +
Generated Media
+ + {% for generated_media in task.generated_media %} + + + + + + + + + + + +
ID + {{ generated_media.id }} +
Type{{ generated_media.type }}
+ {% endfor %} + {% endif %} + {% endfor %} +
+
+
+ {% endif %} + + {% if document.pages|length > 0 %} +
+
+

+ +

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

Method: {{ page.capture_method }}

+
+
+ {% endif %} + {% endfor %} +
+
+
+
+ {% endif %} +
+
+
+ {% endfor %} + {% endwith %} + + {% if session_result.resources.zoom_liveness_resources|length > 0 %} +
+
+

Zoom Liveness Resources

+
+
+ {% endif %} + + {% with liveness_num=0 %} + {% for liveness in session_result.resources.zoom_liveness_resources %} + {% set liveness_num = loop.index + 1 %} +
+
+ + + + + + + +
ID{{ liveness.id }}
+ +
+ + {% 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 %} +
+
+

+ +

+
+
+
+ {% for frame in liveness.frames %} + {% if frame.media is not none %} +
+ +
+
Frame
+
+
+ {% endif %} + {% endfor %} +
+
+
+ {% endif %} +
+
+
+ {% endfor %} + {% endwith %} +
+{% include "layout/footer.html" %} \ No newline at end of file diff --git a/examples/yoti_example_django/README.md b/examples/yoti_example_django/README.md new file mode 100644 index 00000000..2b751d2c --- /dev/null +++ b/examples/yoti_example_django/README.md @@ -0,0 +1,7 @@ +### Django Profile Example Project + +1. Rename the [.env.example](.env.example) file to `.env` and fill in the required configuration values +1. Install dependencies: `pip install -r requirements.txt` +1. Apply migrations before the first start by running: `python manage.py migrate` +1. Run: `python manage.py runsslserver 0.0.0.0:5000` +1. Navigate to https://localhost:5000 \ No newline at end of file diff --git a/examples/yoti_example_flask/README.md b/examples/yoti_example_flask/README.md new file mode 100644 index 00000000..2589ede5 --- /dev/null +++ b/examples/yoti_example_flask/README.md @@ -0,0 +1,6 @@ +#### Flask Profile Example Project + +1. Rename the [.env.example](.env.example) file to `.env` and fill in the required configuration values +1. Install dependencies: `pip install -r requirements.txt` +1. Run `python app.py` +1. Navigate to https://localhost:5000 \ No newline at end of file diff --git a/sonar-project.properties b/sonar-project.properties index ab534cae..4869bb67 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.11.2 +sonar.projectVersion = 2.12.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/client.py b/yoti_python_sdk/doc_scan/client.py index 66112662..e7d406a4 100644 --- a/yoti_python_sdk/doc_scan/client.py +++ b/yoti_python_sdk/doc_scan/client.py @@ -15,6 +15,7 @@ from yoti_python_sdk.http import SignedRequest from yoti_python_sdk.utils import YotiEncoder from .exception import DocScanException +from .support import SupportedDocumentsResponse class DocScanClient(object): @@ -169,3 +170,26 @@ def delete_media_content(self, session_id, media_id): response = request.execute() if response.status_code < 200 or response.status_code >= 300: raise DocScanException("Failed to delete media content", response) + + def get_supported_documents(self): + """ + Retrieves a list of all of the currently supported documents + + :return: the supported documents response + :rtype: SupportedDocumentsResponse + """ + request = ( + SignedRequest.builder() + .with_http_method("GET") + .with_pem_file(self.__key) + .with_base_url(self.__api_url) + .with_endpoint(Endpoint.get_supported_documents_path()) + .build() + ) + + response = request.execute() + if response.status_code < 200 or response.status_code >= 300: + raise DocScanException("Failed to retrieve supported documents", response) + + parsed = json.loads(response.text) + return SupportedDocumentsResponse(parsed) diff --git a/yoti_python_sdk/doc_scan/constants.py b/yoti_python_sdk/doc_scan/constants.py index 3af3a8c3..9cdd96c8 100644 --- a/yoti_python_sdk/doc_scan/constants.py +++ b/yoti_python_sdk/doc_scan/constants.py @@ -16,6 +16,12 @@ CHECK_COMPLETION = "CHECK_COMPLETION" SESSION_COMPLETION = "SESSION_COMPLETION" +ID_DOCUMENT = "ID_DOCUMENT" +ORTHOGONAL_RESTRICTIONS = "ORTHOGONAL_RESTRICTIONS" +DOCUMENT_RESTRICTIONS = "DOCUMENT_RESTRICTIONS" +INCLUSION_WHITELIST = "WHITELIST" +INCLUSION_BLACKLIST = "BLACKLIST" + ALWAYS = "ALWAYS" FALLBACK = "FALLBACK" NEVER = "NEVER" diff --git a/yoti_python_sdk/doc_scan/endpoint.py b/yoti_python_sdk/doc_scan/endpoint.py index 74425fd6..4239bf2f 100644 --- a/yoti_python_sdk/doc_scan/endpoint.py +++ b/yoti_python_sdk/doc_scan/endpoint.py @@ -20,3 +20,7 @@ def get_media_content_path(session_id, media_id): @staticmethod def delete_media_path(session_id, media_id): return Endpoint.get_media_content_path(session_id, media_id) + + @staticmethod + def get_supported_documents_path(): + return "/supported-documents" diff --git a/yoti_python_sdk/doc_scan/session/create/filter/__init__.py b/yoti_python_sdk/doc_scan/session/create/filter/__init__.py new file mode 100644 index 00000000..222e9083 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/filter/__init__.py @@ -0,0 +1,13 @@ +from .document_restrictions_filter import ( + DocumentRestrictionBuilder, + DocumentRestrictionsFilterBuilder, +) +from .orthogonal_restrictions_filter import OrthogonalRestrictionsFilterBuilder +from .required_id_document import RequiredIdDocumentBuilder + +__all__ = [ + 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 new file mode 100644 index 00000000..d42066a8 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/filter/document_filter.py @@ -0,0 +1,17 @@ +from abc import ABCMeta + +from yoti_python_sdk.utils import YotiSerializable + + +class DocumentFilter(YotiSerializable): + __metaclass__ = ABCMeta + + def __init__(self, filter_type): + self.__filter_type = filter_type + + @property + def type(self): + return self.__filter_type + + def to_json(self): + return {"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 new file mode 100644 index 00000000..1d597808 --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/filter/document_restrictions_filter.py @@ -0,0 +1,154 @@ +from yoti_python_sdk.doc_scan.constants import ( + DOCUMENT_RESTRICTIONS, + INCLUSION_BLACKLIST, + INCLUSION_WHITELIST, +) +from yoti_python_sdk.utils import YotiSerializable +from .document_filter import DocumentFilter + + +class DocumentRestriction(YotiSerializable): + def __init__(self, country_codes, document_types): + self.__country_codes = country_codes + self.__document_types = document_types + + @property + def country_codes(self): + return self.__country_codes + + @property + def document_types(self): + return self.__document_types + + def to_json(self): + return { + "country_codes": self.country_codes, + "document_types": self.document_types, + } + + +class DocumentRestrictionBuilder(object): + def __init__(self): + self.__country_codes = None + self.__document_types = None + + def with_country_codes(self, country_codes): + """ + Sets the list of country codes on the document restriction + + :param country_codes: the list of country codes + :type country_codes: list[str] + :return: the builder + :rtype: DocumentRestrictionBuilder + """ + self.__country_codes = country_codes + return self + + def with_document_types(self, document_types): + """ + Sets the list of document types on the document restriction + + :param document_types: the list of document types + :type document_types: list[str] + :return: the builder + :rtype: DocumentRestrictionBuilder + """ + self.__document_types = document_types + return self + + def build(self): + """ + Builds the document restriction using the values supplied to the builder + + :return: the document restriction + :rtype: DocumentRestriction + """ + return DocumentRestriction(self.__country_codes, self.__document_types) + + +class DocumentRestrictionsFilter(DocumentFilter): + def __init__(self, inclusion, documents): + DocumentFilter.__init__(self, DOCUMENT_RESTRICTIONS) + + self.__inclusion = inclusion + self.__documents = documents + + @property + def inclusion(self): + return self.__inclusion + + @property + def documents(self): + return self.__documents + + def to_json(self): + parent = DocumentFilter.to_json(self) + parent["inclusion"] = self.inclusion + parent["documents"] = self.documents + return parent + + +class DocumentRestrictionsFilterBuilder(object): + """ + Builder used to create a document restrictions filter. + + Example:: + + document_restriction = (DocumentRestrictionBuilder() + .with_country_codes("GBR", "USA") + .with_document_types("PASSPORT") + .build()) + + filter = (DocumentRestrictionsFilterBuilder() + .for_whitelist() + .with_document_restriction(document_restriction) + .build()) + """ + + def __init__(self): + self.__inclusion = None + self.__documents = None + + def for_whitelist(self): + """ + Sets the inclusion to whitelist the document restrictions + + :return: the builder + :rtype: DocumentRestrictionsFilterBuilder + """ + self.__inclusion = INCLUSION_WHITELIST + return self + + def for_blacklist(self): + """ + Sets the inclusion to blacklist the document restrictions + + :return: the builder + :rtype: DocumentRestrictionsFilterBuilder + """ + self.__inclusion = INCLUSION_BLACKLIST + return self + + def with_document_restriction(self, document_restriction): + """ + Adds a document restriction to the filter + + :param document_restriction: the document restriction + :type document_restriction: DocumentRestriction + :return: the builder + :rtype: DocumentRestrictionsFilterBuilder + """ + if self.__documents is None: + self.__documents = [] + + self.__documents.append(document_restriction) + return self + + def build(self): + """ + Builds the document restrictions filter, using the supplied values + + :return: the filter + :rtype: DocumentRestrictionsFilter + """ + return DocumentRestrictionsFilter(self.__inclusion, self.__documents) 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 new file mode 100644 index 00000000..4782cd4a --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/filter/orthogonal_restrictions_filter.py @@ -0,0 +1,178 @@ +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 .document_filter import DocumentFilter + + +class CountryRestriction(YotiSerializable): + def __init__(self, inclusion, country_codes): + self.__inclusion = inclusion + self.__country_codes = country_codes + + @property + def inclusion(self): + """ + Returns the inclusion for the country restriction + + :return: the inclusion + :rtype: str + """ + return self.__inclusion + + @property + def country_codes(self): + """ + Returns the country codes for the restriction + + :return: the country codes + :rtype: list[str] + """ + return self.__country_codes + + def to_json(self): + return {"inclusion": self.inclusion, "country_codes": self.country_codes} + + +class TypeRestriction(YotiSerializable): + def __init__(self, inclusion, document_types): + self.__inclusion = inclusion + self.__document_types = document_types + + @property + def inclusion(self): + """ + Returns the inclusion for the type restriction + + :return: the inclusion + :rtype: str + """ + return self.__inclusion + + @property + def document_types(self): + """ + Returns the document types for the restriction + + :return: the document types + :rtype: list[str] + """ + return self.__document_types + + def to_json(self): + return {"inclusion": self.inclusion, "document_types": self.document_types} + + +class OrthogonalRestrictionsFilter(DocumentFilter): + def __init__(self, country_restriction, type_restriction): + DocumentFilter.__init__(self, filter_type=ORTHOGONAL_RESTRICTIONS) + + self.__country_restriction = country_restriction + self.__type_restriction = type_restriction + + @property + def country_restriction(self): + """ + Returns the country restriction for the orthogonal filter + + :return: the country restriction + :rtype: CountryRestriction + """ + return self.__country_restriction + + @property + def type_restriction(self): + """ + Returns the document type restriction for the orthogonal filter + + :return: the document type restriction + :rtype: TypeRestriction + """ + return self.__type_restriction + + def to_json(self): + parent = DocumentFilter.to_json(self) + parent["country_restriction"] = self.country_restriction + parent["type_restriction"] = self.type_restriction + return parent + + +class OrthogonalRestrictionsFilterBuilder(object): + """ + Builder used to create an orthogonal restriction filter. + + Example:: + + filter = (OrthogonalRestrictionsFilterBuilder() + .with_whitelisted_country_codes(["GBR", "USA"]) + .with_whitelisted_document_types(["PASSPORT"]) + .build()) + + """ + + def __init__(self): + self.__country_restriction = None + self.__type_restriction = None + + def with_whitelisted_country_codes(self, country_codes): + """ + Sets a whitelisted list of country codes on the filter + + :param country_codes: List of country codes + :type country_codes: list[str] + :return: the builder + :rtype: OrthogonalRestrictionsFilterBuilder + """ + self.__country_restriction = CountryRestriction( + INCLUSION_WHITELIST, country_codes + ) + return self + + def with_blacklisted_country_codes(self, country_codes): + """ + Sets a blacklisted list of country codes on the filter + + :param country_codes: list of country codes + :type country_codes: list[str] + :return: the builder + :rtype: OrthogonalRestrictionsFilterBuilder + """ + self.__country_restriction = CountryRestriction( + INCLUSION_BLACKLIST, country_codes + ) + return self + + def with_whitelisted_document_types(self, document_types): + """ + Sets a whitelisted list of document types on the filter + + :param document_types: list of document types + :type document_types: list[str] + :return: the builder + :rtype: OrthogonalRestrictionsFilterBuilder + """ + self.__type_restriction = TypeRestriction(INCLUSION_WHITELIST, document_types) + return self + + def with_blacklisted_document_types(self, document_types): + """ + Sets a blacklisted list of document types on the filter + + :param document_types: list of document types + :type document_types: list[str] + :return: the builder + :rtype: OrthogonalRestrictionsFilterBuilder + """ + self.__type_restriction = TypeRestriction(INCLUSION_BLACKLIST, document_types) + return self + + def build(self): + """ + Builds the orthogonal filter, using the supplied whitelisted/blacklisted values + + :return: the built filter + :rtype: OrthogonalRestrictionsFilter + """ + return OrthogonalRestrictionsFilter( + self.__country_restriction, self.__type_restriction + ) diff --git a/yoti_python_sdk/doc_scan/session/create/filter/required_document.py b/yoti_python_sdk/doc_scan/session/create/filter/required_document.py new file mode 100644 index 00000000..f0eb5bff --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/filter/required_document.py @@ -0,0 +1,18 @@ +from abc import ABCMeta +from abc import abstractmethod + +from yoti_python_sdk.utils import YotiSerializable + + +class RequiredDocument(YotiSerializable): + __metaclass__ = ABCMeta + + @property + @abstractmethod + def type(self): + raise NotImplementedError + + def __new__(cls, *args, **kwargs): + if cls is RequiredDocument: + raise TypeError("RequiredDocument may not be instantiated") + return object.__new__(cls) 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 new file mode 100644 index 00000000..3bd3c64d --- /dev/null +++ b/yoti_python_sdk/doc_scan/session/create/filter/required_id_document.py @@ -0,0 +1,60 @@ +from yoti_python_sdk.doc_scan.constants import ID_DOCUMENT +from .document_filter import DocumentFilter # noqa: F401 +from .required_document import RequiredDocument + + +class RequiredIdDocument(RequiredDocument): + def __init__(self, doc_filter=None): + """ + :param doc_filter: the filter for the document + :type doc_filter: + """ + self.__doc_filter = doc_filter + + @property + def type(self): + return ID_DOCUMENT + + @property + def filter(self): + return self.__doc_filter + + def to_json(self): + return {"type": self.type, "filter": self.__doc_filter} + + +class RequiredIdDocumentBuilder(object): + """ + Builder used to assist the creation of a required identity document. + + Example:: + + required_id_document = (RequiredIdDocumentBuilder() + .with_filter(some_filter) + .build()) + + """ + + def __init__(self): + self.__id_document_filter = None + + def with_filter(self, id_document_filter): + """ + Sets the filter on the required ID document + + :param id_document_filter: the filter + :type id_document_filter: DocumentFilter + :return: the builder + :rtype: RequiredIdDocumentBuilder + """ + self.__id_document_filter = id_document_filter + return self + + def build(self): + """ + Builds a required ID document, using the values supplied to the builder + + :return: the required ID document + :rtype: RequiredIdDocument + """ + return RequiredIdDocument(self.__id_document_filter) 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 eb7aa90d..0a7f47c7 100644 --- a/yoti_python_sdk/doc_scan/session/create/session_spec.py +++ b/yoti_python_sdk/doc_scan/session/create/session_spec.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- from __future__ import unicode_literals +from .filter.required_document import RequiredDocument # noqa: F401 from yoti_python_sdk.utils import YotiSerializable @@ -18,6 +19,7 @@ def __init__( sdk_config, requested_checks=None, requested_tasks=None, + required_documents=None, ): """ :param client_session_token_ttl: the client session token TTL @@ -31,14 +33,18 @@ def __init__( :param sdk_config: the SDK configuration :type sdk_config: SdkConfig :param requested_checks: the list of requested checks - :type requested_checks: list[RequestedCheck] + :type requested_checks: list[RequestedCheck] or None :param requested_tasks: the list of requested tasks - :type requested_tasks: list[RequestedTask] + :type requested_tasks: list[RequestedTask] or None + :param required_documents: the list of required documents + :type required_documents: list[RequiredDocument] or None """ if requested_tasks is None: requested_tasks = [] if requested_checks is None: requested_checks = [] + if required_documents is None: + required_documents = [] self.__client_session_token_ttl = client_session_token_ttl self.__resources_ttl = resources_ttl @@ -47,6 +53,7 @@ def __init__( self.__sdk_config = sdk_config self.__requested_checks = requested_checks self.__requested_tasks = requested_tasks + self.__required_documents = required_documents @property def client_session_token_ttl(self): @@ -120,6 +127,17 @@ def requested_tasks(self): """ return self.__requested_tasks + @property + def required_documents(self): + """ + List of documents that are required from the user to satisfy a sessions + requirements. + + :return: the list of required documents + :rtype: list[RequiredDocument] + """ + return self.__required_documents + def to_json(self): return { "client_session_token_ttl": self.client_session_token_ttl, @@ -129,6 +147,7 @@ def to_json(self): "requested_checks": self.requested_checks, "requested_tasks": self.requested_tasks, "sdk_config": self.sdk_config, + "required_documents": self.required_documents, } @@ -145,6 +164,7 @@ def __init__(self): self.__sdk_config = None self.__requested_checks = [] self.__requested_tasks = [] + self.__required_documents = [] def with_client_session_token_ttl(self, value): """ @@ -230,6 +250,18 @@ def with_sdk_config(self, value): self.__sdk_config = value return self + def with_required_document(self, required_document): + """ + Adds a required document to the session specification + + :param required_document: the required document + :type required_document: RequiredDocument + :return: the builder + :rtype: SessionSpecBuilder + """ + self.__required_documents.append(required_document) + return self + def build(self): """ Builds a :class:`SessionSpec` using the supplied values @@ -245,4 +277,5 @@ def build(self): self.__sdk_config, self.__requested_checks, self.__requested_tasks, + self.__required_documents, ) 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 33bc054f..156e14ad 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 @@ -6,6 +6,9 @@ ) 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 ( + TextExtractionTaskResponse, +) class IdDocumentResourceResponse(ResourceResponse): @@ -71,3 +74,16 @@ def document_fields(self): :rtype: DocumentFieldsResponse """ return self.__document_fields + + @property + def text_extraction_tasks(self): + """ + Returns a list of text extraction tasks associated + with the id document + + :return: list of text extraction tasks + :rtype: list[TextExtractionTaskResponse] + """ + return [ + task for task in self.tasks if isinstance(task, TextExtractionTaskResponse) + ] 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 index 7f629475..a214396b 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/liveness_resource_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/liveness_resource_response.py @@ -11,7 +11,17 @@ class LivenessResourceResponse(ResourceResponse): Represents a Liveness resource for a given session """ - pass + def __init__(self, data=None): + if data is None: + data = dict() + + ResourceResponse.__init__(self, data) + + self.__liveness_type = data.get("liveness_type", None) + + @property + def liveness_type(self): + return self.__liveness_type class ZoomLivenessResourceResponse(LivenessResourceResponse): diff --git a/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py b/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py index ce60cc28..c2efbfe1 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py @@ -6,8 +6,6 @@ ) 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, ) @@ -75,3 +73,17 @@ def liveness_capture(self): :rtype: list[LivenessResourceResponse] """ return self.__liveness_capture + + @property + def zoom_liveness_resources(self): + """ + Returns a filtered list of zoom liveness capture resources + + :return: list of zoom liveness captures + :rtype: list[ZoomLivenessResourceResponse] + """ + return [ + liveness + for liveness in self.__liveness_capture + if isinstance(liveness, ZoomLivenessResourceResponse) + ] diff --git a/yoti_python_sdk/doc_scan/session/retrieve/task_response.py b/yoti_python_sdk/doc_scan/session/retrieve/task_response.py index 9511dc4b..43dc5bb6 100644 --- a/yoti_python_sdk/doc_scan/session/retrieve/task_response.py +++ b/yoti_python_sdk/doc_scan/session/retrieve/task_response.py @@ -156,4 +156,10 @@ class TextExtractionTaskResponse(TaskResponse): Represents a Text Extraction task response """ - pass + @property + def generated_text_data_checks(self): + return [ + check + for check in self.generated_checks + if isinstance(check, GeneratedTextDataCheckResponse) + ] diff --git a/yoti_python_sdk/doc_scan/support/__init__.py b/yoti_python_sdk/doc_scan/support/__init__.py new file mode 100644 index 00000000..eba661aa --- /dev/null +++ b/yoti_python_sdk/doc_scan/support/__init__.py @@ -0,0 +1,3 @@ +from .supported_documents import SupportedDocumentsResponse + +__all__ = [SupportedDocumentsResponse] diff --git a/yoti_python_sdk/doc_scan/support/supported_documents.py b/yoti_python_sdk/doc_scan/support/supported_documents.py new file mode 100644 index 00000000..55505b78 --- /dev/null +++ b/yoti_python_sdk/doc_scan/support/supported_documents.py @@ -0,0 +1,44 @@ +class SupportedDocument(object): + def __init__(self, data=None): + if data is None: + data = dict() + + self.__type = data.get("type", None) + + @property + def type(self): + return self.__type + + +class SupportedCountry(object): + def __init__(self, data=None): + if data is None: + data = dict() + + self.__code = data.get("code", None) + self.__supported_documents = [ + SupportedDocument(document) + for document in data.get("supported_documents", []) + ] + + @property + def code(self): + return self.__code + + @property + def supported_documents(self): + return self.__supported_documents + + +class SupportedDocumentsResponse(object): + def __init__(self, data=None): + if data is None: + data = dict() + + self.__supported_countries = [ + SupportedCountry(country) for country in data.get("supported_countries", []) + ] + + @property + def supported_countries(self): + return self.__supported_countries diff --git a/yoti_python_sdk/tests/conftest.py b/yoti_python_sdk/tests/conftest.py index 86f080cf..16748308 100644 --- a/yoti_python_sdk/tests/conftest.py +++ b/yoti_python_sdk/tests/conftest.py @@ -1,8 +1,6 @@ # -*- coding: utf-8 -*- import io -from os.path import abspath -from os.path import dirname -from os.path import join +from os.path import abspath, dirname, join import pytest diff --git a/yoti_python_sdk/tests/doc_scan/fixtures/supported_documents_success.txt b/yoti_python_sdk/tests/doc_scan/fixtures/supported_documents_success.txt new file mode 100644 index 00000000..57a09216 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/fixtures/supported_documents_success.txt @@ -0,0 +1 @@ +{"supported_countries":[{"code":"GBR"},{"code":"USA"}]} \ 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 index e3be56d1..b5e2a358 100644 --- a/yoti_python_sdk/tests/doc_scan/mocks.py +++ b/yoti_python_sdk/tests/doc_scan/mocks.py @@ -42,5 +42,11 @@ def mocked_request_missing_content(): return MockResponse(status_code=404, text="") +def mocked_supported_documents_content(): + with open(FIXTURES_DIR + "/supported_documents_success.txt", "r") as f: + response = f.read() + return MockResponse(status_code=200, text=response) + + def mocked_request_server_error(): return MockResponse(status_code=500, text="") diff --git a/yoti_python_sdk/tests/doc_scan/session/__init__.py b/yoti_python_sdk/tests/doc_scan/session/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/doc_scan/session/create/__init__.py b/yoti_python_sdk/tests/doc_scan/session/create/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/doc_scan/session/create/check/__init__.py b/yoti_python_sdk/tests/doc_scan/session/create/check/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/doc_scan/session/create/filter/__init__.py b/yoti_python_sdk/tests/doc_scan/session/create/filter/__init__.py new file mode 100644 index 00000000..e69de29b 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 new file mode 100644 index 00000000..d761827d --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_document_restriction_builder.py @@ -0,0 +1,43 @@ +import json + +from yoti_python_sdk.doc_scan.session.create.filter import DocumentRestrictionBuilder +from yoti_python_sdk.utils import YotiEncoder + + +def test_should_build_without_setting_properties(): + result = DocumentRestrictionBuilder().build() + + assert result.document_types is None + assert result.country_codes is None + + +def test_with_country_codes(): + result = ( + DocumentRestrictionBuilder().with_country_codes(["GBR", "USA", "UKR"]).build() + ) + + assert len(result.country_codes) == 3 + assert result.country_codes == ["GBR", "USA", "UKR"] + + +def test_with_document_types(): + result = ( + DocumentRestrictionBuilder() + .with_document_types(["PASSPORT", "DRIVING_LICENCE"]) + .build() + ) + + assert len(result.document_types) == 2 + assert result.document_types == ["PASSPORT", "DRIVING_LICENCE"] + + +def test_to_json_should_not_throw_exception(): + result = ( + DocumentRestrictionBuilder() + .with_country_codes(["GBR", "USA", "UKR"]) + .with_document_types(["PASSPORT", "DRIVING_LICENCE"]) + .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_restrictions_filter.py b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_document_restrictions_filter.py new file mode 100644 index 00000000..1c3745ee --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_document_restrictions_filter.py @@ -0,0 +1,67 @@ +import json + +from mock import Mock + +from yoti_python_sdk.doc_scan.session.create.filter import ( + DocumentRestrictionsFilterBuilder, +) +from yoti_python_sdk.doc_scan.session.create.filter.document_restrictions_filter import ( + DocumentRestriction, +) +from yoti_python_sdk.utils import YotiEncoder + + +def test_should_set_inclusion_to_whitelist(): + result = DocumentRestrictionsFilterBuilder().for_whitelist().build() + + assert result.inclusion == "WHITELIST" + + +def test_should_set_inclusion_to_blacklist(): + result = DocumentRestrictionsFilterBuilder().for_blacklist().build() + + assert result.inclusion == "BLACKLIST" + + +def test_should_accept_document_restriction(): + document_restriction_mock = Mock(spec=DocumentRestriction) + + result = ( + DocumentRestrictionsFilterBuilder() + .for_whitelist() + .with_document_restriction(document_restriction_mock) + .build() + ) + + assert len(result.documents) == 1 + assert result.documents[0] == document_restriction_mock + + +def test_should_accept_multiple_document_restrictions(): + document_restriction_mock = Mock(spec=DocumentRestriction) + other_document_restriction_mock = Mock(spec=DocumentRestriction) + + result = ( + DocumentRestrictionsFilterBuilder() + .for_whitelist() + .with_document_restriction(document_restriction_mock) + .with_document_restriction(other_document_restriction_mock) + .build() + ) + + assert len(result.documents) == 2 + + +def test_to_json_should_not_throw_exception(): + document_restriction_mock = Mock(spec=DocumentRestriction) + document_restriction_mock.to_json.return_value = {} + + result = ( + DocumentRestrictionsFilterBuilder() + .for_whitelist() + .with_document_restriction(document_restriction_mock) + .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_orthogonal_restrictions_filter.py b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_orthogonal_restrictions_filter.py new file mode 100644 index 00000000..123e9adc --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_orthogonal_restrictions_filter.py @@ -0,0 +1,70 @@ +import json + +from yoti_python_sdk.doc_scan.session.create.filter import ( + OrthogonalRestrictionsFilterBuilder, +) +from yoti_python_sdk.doc_scan.session.create.filter.orthogonal_restrictions_filter import ( + CountryRestriction, + TypeRestriction, +) +from yoti_python_sdk.utils import YotiEncoder + + +def test_should_create_correct_country_restriction(): + result = ( + OrthogonalRestrictionsFilterBuilder() + .with_whitelisted_country_codes(["GBR", "USA"]) + .build() + ) + + assert isinstance(result.country_restriction, CountryRestriction) + assert result.country_restriction.inclusion == "WHITELIST" + assert result.country_restriction.country_codes == ["GBR", "USA"] + + +def test_should_create_correct_type_restriction(): + result = ( + OrthogonalRestrictionsFilterBuilder() + .with_whitelisted_document_types(["PASSPORT", "DRIVING_LICENCE"]) + .build() + ) + + assert isinstance(result.type_restriction, TypeRestriction) + assert result.type_restriction.inclusion == "WHITELIST" + assert result.type_restriction.document_types == ["PASSPORT", "DRIVING_LICENCE"] + + +def test_should_set_inclusion_to_whitelist(): + result = ( + OrthogonalRestrictionsFilterBuilder() + .with_whitelisted_document_types([]) + .with_whitelisted_country_codes([]) + .build() + ) + + assert result.country_restriction.inclusion == "WHITELIST" + assert result.type_restriction.inclusion == "WHITELIST" + + +def test_should_set_inclusion_to_blacklist(): + result = ( + OrthogonalRestrictionsFilterBuilder() + .with_blacklisted_country_codes([]) + .with_blacklisted_document_types([]) + .build() + ) + + assert result.country_restriction.inclusion == "BLACKLIST" + assert result.type_restriction.inclusion == "BLACKLIST" + + +def test_to_json_should_not_throw_exception(): + result = ( + OrthogonalRestrictionsFilterBuilder() + .with_whitelisted_document_types(["PASSPORT", "DRIVING_LICENCE"]) + .with_whitelisted_country_codes(["GBR", "USA", "UKR"]) + .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_required_document.py b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_document.py new file mode 100644 index 00000000..29e3ad87 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_document.py @@ -0,0 +1,12 @@ +import pytest + +from yoti_python_sdk.doc_scan.session.create.filter.required_document import ( + RequiredDocument, +) + + +def test_should_not_allow_direct_instantiation(): + with pytest.raises(TypeError) as e: + RequiredDocument() + + assert str(e.value) == "RequiredDocument may not be instantiated" diff --git a/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_id_document.py b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_id_document.py new file mode 100644 index 00000000..72d8ee07 --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/session/create/filter/test_required_id_document.py @@ -0,0 +1,44 @@ +import json + +from mock import Mock + +from yoti_python_sdk.doc_scan.session.create.filter.document_filter import ( + DocumentFilter, +) +from yoti_python_sdk.doc_scan.session.create.filter.orthogonal_restrictions_filter import ( + OrthogonalRestrictionsFilter, +) +from yoti_python_sdk.doc_scan.session.create.filter.required_document import ( + RequiredDocument, +) +from yoti_python_sdk.doc_scan.session.create.filter.required_id_document import ( + RequiredIdDocument, + RequiredIdDocumentBuilder, +) +from yoti_python_sdk.utils import YotiEncoder + + +def test_should_allow_direct_instantiation(): + doc_filter_mock = Mock(spec=OrthogonalRestrictionsFilter) + + result = RequiredIdDocument(doc_filter_mock) + assert result.type == "ID_DOCUMENT" + assert result.filter == doc_filter_mock + + +def test_builder_should_accept_any_document_filter(): + doc_filter = DocumentFilter("SOME_FILTER") + + result = RequiredIdDocumentBuilder().with_filter(doc_filter).build() + + assert isinstance(result, RequiredDocument) + assert result.filter == doc_filter + + +def test_to_json_should_not_raise_exception(): + doc_filter = DocumentFilter("SOME_FILTER") + + result = RequiredIdDocumentBuilder().with_filter(doc_filter).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/task/__init__.py b/yoti_python_sdk/tests/doc_scan/session/create/task/__init__.py new file mode 100644 index 00000000..e69de29b 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 b63ae1a3..9130a8ca 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 @@ -5,10 +5,14 @@ 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.filter.required_id_document import ( + RequiredIdDocument, +) 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.session_spec import SessionSpec from yoti_python_sdk.doc_scan.session.create.task.requested_task import RequestedTask from yoti_python_sdk.utils import YotiEncoder @@ -74,6 +78,32 @@ 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_add_required_document(self): + required_document_mock = Mock(spec=RequiredIdDocument) + + result = ( + SessionSpecBuilder().with_required_document(required_document_mock).build() + ) + + assert len(result.required_documents) == 1 + assert result.required_documents[0] == required_document_mock + + def test_should_default_empty_arrays(self): + result = SessionSpec( + client_session_token_ttl=1, + resources_ttl=1, + user_tracking_id="someTrackingId", + notifications=None, + sdk_config=None, + requested_checks=None, + requested_tasks=None, + required_documents=None, + ) + + assert len(result.requested_checks) == 0 + assert len(result.requested_tasks) == 0 + assert len(result.required_documents) == 0 + 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 index 6840037c..deeac750 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 @@ -68,6 +68,14 @@ def test_should_parse_tasks_with_type(self): assert isinstance(result.tasks[0], TextExtractionTaskResponse) assert isinstance(result.tasks[1], TaskResponse) + def test_should_filter_text_extraction_tasks(self): + data = {"tasks": self.SOME_TASKS} + + result = IdDocumentResourceResponse(data) + + assert len(result.tasks) == 2 + assert len(result.text_extraction_tasks) == 1 + 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 index 78802b07..b3bbc47b 100644 --- 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 @@ -3,13 +3,35 @@ 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, + LivenessResourceResponse, ) class LivenessResourceResponseTest(unittest.TestCase): + def test_should_not_throw_exception_if_data_is_none(self): + result = LivenessResourceResponse(None) + + assert result.liveness_type is None + assert result.id is None + assert len(result.tasks) == 0 + + +class ZoomLivenessResourceResponseTest(unittest.TestCase): SOME_ID = "someId" SOME_FRAMES = [{"first": "frame"}, {"second": "frame"}] + def test_should_retain_liveness_type(self): + data = { + "id": self.SOME_ID, + "facemap": {}, + "frames": self.SOME_FRAMES, + "liveness_type": "ZOOM", + } + + result = ZoomLivenessResourceResponse(data) + + assert result.liveness_type == "ZOOM" + def test_zoom_liveness_should_parse_correctly(self): data = {"id": self.SOME_ID, "facemap": {}, "frames": self.SOME_FRAMES} 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 index b1507953..d397d8c5 100644 --- 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 @@ -34,6 +34,19 @@ def test_should_parse_with_none(self): assert len(result.id_documents) == 0 assert len(result.liveness_capture) == 0 + def test_should_filter_zoom_liveness_resources(self): + data = { + "liveness_capture": [ + {"liveness_type": "ZOOM"}, + {"liveness_type": "someUnknown"}, + ] + } + + result = ResourceContainer(data) + + assert len(result.liveness_capture) == 2 + assert len(result.zoom_liveness_resources) == 1 + 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 index 5ef4029e..7d91b20d 100644 --- 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 @@ -9,7 +9,10 @@ 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 +from yoti_python_sdk.doc_scan.session.retrieve.task_response import ( + TaskResponse, + TextExtractionTaskResponse, +) class TaskResponseTest(unittest.TestCase): @@ -74,6 +77,14 @@ def test_should_parse_with_none(self): assert len(result.generated_checks) == 0 assert len(result.generated_media) == 0 + def test_should_filter_generated_text_data_checks(self): + data = {"generated_checks": self.SOME_GENERATED_CHECKS} + + result = TextExtractionTaskResponse(data) + + assert len(result.generated_checks) == 2 + assert len(result.generated_text_data_checks) == 1 + if __name__ == "__main__": unittest.main() diff --git a/yoti_python_sdk/tests/doc_scan/support/__init__.py b/yoti_python_sdk/tests/doc_scan/support/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/doc_scan/support/test_supported_documents.py b/yoti_python_sdk/tests/doc_scan/support/test_supported_documents.py new file mode 100644 index 00000000..762d509d --- /dev/null +++ b/yoti_python_sdk/tests/doc_scan/support/test_supported_documents.py @@ -0,0 +1,51 @@ +from yoti_python_sdk.doc_scan.support.supported_documents import ( + SupportedCountry, + SupportedDocument, + SupportedDocumentsResponse, +) + + +def test_supported_document_should_parse_data(): + data = {"type": "someSupportedDocument"} + + result = SupportedDocument(data) + + assert result.type == "someSupportedDocument" + + +def test_supported_document_should_not_throw_exception_on_missing_data(): + result = SupportedDocument(None) + assert result.type is None + + +def test_supported_country_should_parse_data(): + data = { + "code": "someCode", + "supported_documents": [{"type": "firstType"}, {"type": "secondType"}], + } + + result = SupportedCountry(data) + + assert result.code == "someCode" + assert len(result.supported_documents) == 2 + + +def test_supported_country_should_not_throw_exception_on_missing_data(): + result = SupportedCountry(None) + + assert result.code is None + assert len(result.supported_documents) == 0 + + +def test_supported_document_response_should_parse_data(): + data = {"supported_countries": [{"code": "GBR"}, {"code": "USA"}]} + + result = SupportedDocumentsResponse(data) + + assert len(result.supported_countries) == 2 + + +def test_supported_document_response_should_not_throw_exception_on_missing_data(): + result = SupportedDocumentsResponse(None) + + assert len(result.supported_countries) == 0 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 index e8d31bdb..11eefc9c 100644 --- a/yoti_python_sdk/tests/doc_scan/test_doc_scan_client.py +++ b/yoti_python_sdk/tests/doc_scan/test_doc_scan_client.py @@ -9,16 +9,15 @@ 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_failed_session_creation, + mocked_request_failed_session_retrieval, + mocked_request_media_content, + mocked_request_missing_content, + mocked_request_server_error, mocked_request_successful_session_creation, -) -from yoti_python_sdk.tests.doc_scan.mocks import ( mocked_request_successful_session_retrieval, + mocked_supported_documents_content, ) try: @@ -156,6 +155,35 @@ def test_should_throw_exception_for_delete_media(_, doc_scan_client): assert 404 == doc_scan_exception.status_code +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_supported_documents_content, +) +def test_should_return_supported_documents_response(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + supported_docs = doc_scan_client.get_supported_documents() + + assert len(supported_docs.supported_countries) == 2 + + +@mock.patch( + "yoti_python_sdk.http.SignedRequest.execute", + side_effect=mocked_request_missing_content, +) +def test_should_throw_exception_for_supported_documents(_, doc_scan_client): + """ + :type doc_scan_client: DocScanClient + """ + with pytest.raises(DocScanException) as ex: + doc_scan_client.get_supported_documents() + + doc_scan_exception = ex.value + assert "Failed to retrieve supported documents" in str(doc_scan_exception) + assert 404 == doc_scan_exception.status_code + + def test_should_use_correct_default_api_url(doc_scan_client): assert ( doc_scan_client._DocScanClient__api_url diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/__init__.py b/yoti_python_sdk/tests/dynamic_sharing_service/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/extension/__init__.py b/yoti_python_sdk/tests/dynamic_sharing_service/extension/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/policy/__init__.py b/yoti_python_sdk/tests/dynamic_sharing_service/policy/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/tests/share/__init__.py b/yoti_python_sdk/tests/share/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/yoti_python_sdk/version.py b/yoti_python_sdk/version.py index 9fe7dfef..3c62c652 100644 --- a/yoti_python_sdk/version.py +++ b/yoti_python_sdk/version.py @@ -1,2 +1,2 @@ # -*- coding: utf-8 -*- -__version__ = "2.11.2" +__version__ = "2.12.0"