-
- {% if liveness.frames|length > 0 %}
+ {% for liveness in session_result.resources.zoom_liveness_resources %}
+ {% set liveness_num = loop.index %}
+
+
+
+
+
+ | ID |
+ {{ liveness.id }} |
+
+
+
+
+
+
+ {% if liveness.frames|length > 0 %}
+
+
+
+
+ {% for frame in liveness.frames %}
+ {% if frame.media is not none %}
-
+
+
+ {% endif %}
+
+
+
+ {% endfor %}
+ {% endwith %}
+
+ {% if session_result.resources.static_liveness_resources|length > 0 %}
+
+
+
Static Liveness Resources
+
+
+ {% endif %}
+
+ {% with liveness_num=0 %}
+ {% for liveness in session_result.resources.static_liveness_resources %}
+ {% set liveness_num = loop.index %}
+
+
+
+
+
+ | ID |
+ {{ liveness.id }} |
+
+
+ | Liveness Type |
+ {{ liveness.liveness_type }} |
+
+
+
+
+
+ {% if liveness.image is not none and liveness.image.media is not none %}
+
+
+
+
+
+

+
+
Static Liveness Check
+
Media ID: {{ liveness.image.media.id }}
- {% endif %}
+
+ {% endif %}
- {% endfor %}
+
+
+ {% endfor %}
{% endwith %}
{% include "layout/footer.html" %}
diff --git a/examples/yoti_example_django/requirements.in b/examples/yoti_example_django/requirements.in
index 6210c53b..884d3bd9 100644
--- a/examples/yoti_example_django/requirements.in
+++ b/examples/yoti_example_django/requirements.in
@@ -1,6 +1,8 @@
-django>=3.0.7
+django>=4.0.1
django-sslserver>=0.22.0
python-dotenv>=0.7.1
requests>=2.20.0
urllib3>=1.24.2
-yoti>=2.13.0
+yoti>=2.14.0
+six>=1.16.0
+cffi>=1.15.0
\ No newline at end of file
diff --git a/examples/yoti_example_django/requirements.txt b/examples/yoti_example_django/requirements.txt
index 743d3e65..d966b31c 100644
--- a/examples/yoti_example_django/requirements.txt
+++ b/examples/yoti_example_django/requirements.txt
@@ -1,5 +1,5 @@
#
-# This file is autogenerated by pip-compile
+# This file is autogenerated by pip-compile with python 3.10
# To update, run:
#
# pip-compile --output-file=requirements.txt requirements.in
@@ -8,10 +8,14 @@ asgiref==3.4.1
# via django
asn1==2.2.0
# via yoti
+backports.zoneinfo==0.2.1
+ # via django
certifi==2018.4.16
# via requests
-cffi==1.14.0
- # via cryptography
+cffi==1.15.0
+ # via
+ # -r requirements.in
+ # cryptography
chardet==3.0.4
# via requests
cryptography==3.2
@@ -20,7 +24,7 @@ cryptography==3.2
# yoti
deprecated==1.2.10
# via yoti
-django==3.1.12
+django==4.0.1
# via
# -r requirements.in
# django-sslserver
@@ -40,14 +44,15 @@ pyopenssl==18.0.0
# via yoti
python-dotenv==0.8.2
# via -r requirements.in
-pytz==2018.4
- # via django
+pytz==2020.4
+ # via yoti
requests==2.21.0
# via
# -r requirements.in
# yoti
-six==1.11.0
+six==1.16.0
# via
+ # -r requirements.in
# cryptography
# protobuf
# pyopenssl
@@ -59,7 +64,7 @@ urllib3==1.24.2
# requests
wrapt==1.12.1
# via deprecated
-yoti==2.13.0
+yoti==2.14.0
# via -r requirements.in
# The following packages are considered to be unsafe in a requirements file:
diff --git a/examples/yoti_example_django/yoti_example/urls.py b/examples/yoti_example_django/yoti_example/urls.py
index f8063333..77688930 100644
--- a/examples/yoti_example_django/yoti_example/urls.py
+++ b/examples/yoti_example_django/yoti_example/urls.py
@@ -13,17 +13,17 @@
1. Import the include() function: from django.conf.urls import url, include
2. Add a URL to urlpatterns: url(r'^blog/', include('blog.urls'))
"""
-from django.conf.urls import url
+from django.urls import re_path
from django.contrib import admin
from .views import IndexView, AuthView, DynamicShareView, SourceConstraintsView
urlpatterns = [
- url(r"^$", IndexView.as_view(), name="index"),
- url(r"^yoti/auth/$", AuthView.as_view(), name="auth"),
- url(r"^admin/", admin.site.urls),
- url(r"^dynamic-share/$", DynamicShareView.as_view(), name="dynamic-share"),
- url(
+ re_path(r"^$", IndexView.as_view(), name="index"),
+ re_path(r"^yoti/auth/$", AuthView.as_view(), name="auth"),
+ re_path(r"^admin/", admin.site.urls),
+ re_path(r"^dynamic-share/$", DynamicShareView.as_view(), name="dynamic-share"),
+ re_path(
r"^source-constraint/$",
SourceConstraintsView.as_view(),
name="source-constraints",
diff --git a/requirements.in b/requirements.in
index d7235772..5bdc7fe3 100644
--- a/requirements.in
+++ b/requirements.in
@@ -1,15 +1,16 @@
-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.3
-future==0.18.2
-itsdangerous==1.1.0
-pbr==1.10.0
-protobuf==3.13.0
-pyopenssl==19.1.0
-PyYAML==5.2 # PyYAML 5.3 does not support Python 3.4
-pytz==2022.1
-requests>=2.20.0
-urllib3>=1.24.3
-deprecated==1.2.10
-wheel==0.33.6
-iso8601==0.1.13
+asn1==2.2.0
+cryptography>=41.0.7
+cffi>=1.16.0
+future>=0.18.3
+itsdangerous>=2.1.2
+pbr>=5.11.1
+protobuf==3.20.3
+pyopenssl>=24.0.0
+PyYAML>=6.0
+pytz>=2025.2
+requests>=2.31.0
+urllib3>=2.2.1
+deprecated>=1.2.14
+wheel>=0.41.0
+iso8601>=1.1.0
+wrapt>=1.15.0
diff --git a/requirements.txt b/requirements.txt
index 7d09771c..ab374aef 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -1,6 +1,8 @@
#
-# This file is autogenerated by pip-compile with python 3.9
-# To update, run:
+
+# This file is autogenerated by pip-compile with Python 3.11
+# by the following command:
+
#
# pip-compile --output-file=requirements.txt requirements.in
#
@@ -8,53 +10,63 @@ asn1==2.2.0
# via -r requirements.in
certifi==2018.11.29
# via requests
-cffi==1.14.3
+
+cffi==1.17.1
# via
# -r requirements.in
# cryptography
-chardet==3.0.4
+charset-normalizer==3.4.1
# via requests
-cryptography==2.8
+cryptography==44.0.2
# via
# -r requirements.in
# pyopenssl
-deprecated==1.2.10
+
+deprecated==1.2.18
# via -r requirements.in
-future==0.18.2
+future==1.0.0
# via -r requirements.in
idna==2.7
# via requests
-iso8601==0.1.13
+
+iso8601==2.1.0
# via -r requirements.in
-itsdangerous==1.1.0
+itsdangerous==2.2.0
+
# via -r requirements.in
-pbr==1.10.0
+pbr==6.1.1
# via -r requirements.in
-protobuf==3.13.0
+
+protobuf==3.20.3
+
# via -r requirements.in
pycparser==2.18
# via cffi
-pyopenssl==19.1.0
+pyopenssl==25.0.0
# via -r requirements.in
-pytz==2022.1
+
+pytz==2025.2
+
# via -r requirements.in
-pyyaml==5.2
+pyyaml==6.0.2
# via -r requirements.in
-requests==2.21.0
+requests==2.32.0
# via -r requirements.in
-six==1.10.0
- # via
- # cryptography
- # protobuf
- # pyopenssl
-urllib3==1.24.3
+typing-extensions==4.13.2
+ # via pyopenssl
+urllib3==2.4.0
# via
# -r requirements.in
# requests
-wheel==0.33.6
+wheel==0.45.1
# via -r requirements.in
-wrapt==1.11.2
- # via deprecated
+wrapt==1.17.2
+ # via
+ # -r requirements.in
+ # deprecated
# The following packages are considered to be unsafe in a requirements file:
# setuptools
+
+
+
diff --git a/setup.py b/setup.py
index 5f11c781..4e9bfdf1 100644
--- a/setup.py
+++ b/setup.py
@@ -18,15 +18,16 @@
author="Yoti",
author_email="websdk@yoti.com",
install_requires=[
- "deprecated==1.2.10",
- "cryptography>=2.2.1",
- "protobuf==3.13.0",
- "requests>=2.11.1",
- "future>=0.11.0",
- "asn1==2.2.0",
- "pyopenssl>=18.0.0",
- "iso8601==0.1.13",
- "pytz==2022.1",
+
+ "asn1==2.2.0", # still pinned due to enum34 issue
+ "cryptography>=42.0.0",
+ "protobuf==3.20.3",
+ "requests>=2.31.0",
+ "pyopenssl>=24.0.0",
+ "pytz>=2025.2",
+ "iso8601>=1.1.0",
+ "deprecated>=1.2.14",
+
],
extras_require={
"examples": [
@@ -34,18 +35,20 @@
"Flask>=1.0.4",
"python-dotenv>=0.7.1",
"django-sslserver>=0.22.0",
- "Werkzeug==1.0.1",
+ "Werkzeug==2.1.2",
],
"dev": [
- "pre-commit==1.17.0",
+ "pre-commit==2.16.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",
+ "coverage>=7.4.0",
"mock==2.0.0",
- "virtualenv==20.1.0",
+ "virtualenv==20.15.1",
+ "flake8==4.0.1",
+ "pip-tools==6.6.2",
],
},
classifiers=[
@@ -54,13 +57,12 @@
"Operating System :: OS Independent",
"Intended Audience :: Developers",
"Programming Language :: Python",
- "Programming Language :: Python :: 2",
- "Programming Language :: Python :: 2.7",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.4",
- "Programming Language :: Python :: 3.5",
- "Programming Language :: Python :: 3.6",
- "Programming Language :: Python :: 3.7",
+
+ "Programming Language :: Python :: 3.9",
+ "Programming Language :: Python :: 3.10",
+ "Programming Language :: Python :: 3.11",
+ "Programming Language :: Python :: 3.12",
+
"Topic :: Software Development :: Libraries :: Python Modules",
],
keywords="yoti sdk 2FA multifactor authentication verification identity login register verify 2Factor",
diff --git a/sonar-project.properties b/sonar-project.properties
index e85e5860..f9f4b359 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.14.1
+sonar.projectVersion = 2.14.3
sonar.exclusions = yoti_python_sdk/tests/**,examples/**,yoti_python_sdk/protobuf/**/*
sonar.python.pylint.reportPath = coverage.out
diff --git a/yoti_python_sdk/__init__.py b/yoti_python_sdk/__init__.py
index a33747d5..e2084235 100644
--- a/yoti_python_sdk/__init__.py
+++ b/yoti_python_sdk/__init__.py
@@ -1,8 +1,7 @@
# -*- coding: utf-8 -*-
import os
-from distutils.util import convert_path
from os import environ
-
+from .version import __version__
from yoti_python_sdk.client import Client
DEFAULTS = {
@@ -17,11 +16,8 @@
directory_name = os.path.dirname(__file__)
version_path = os.path.join(directory_name, "version.py")
-ver_path = convert_path(version_path)
-with open(ver_path) as ver_file:
- exec(ver_file.read(), main_ns)
+exec(open(version_path).read())
-__version__ = main_ns["__version__"]
YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"])
YOTI_PROFILE_ENDPOINT = "/api/v1"
diff --git a/yoti_python_sdk/doc_scan/constants.py b/yoti_python_sdk/doc_scan/constants.py
index 6aa18d1d..ec2ea3a3 100644
--- a/yoti_python_sdk/doc_scan/constants.py
+++ b/yoti_python_sdk/doc_scan/constants.py
@@ -8,6 +8,7 @@
ID_DOCUMENT_FACE_MATCH = "ID_DOCUMENT_FACE_MATCH"
LIVENESS = "LIVENESS"
ZOOM = "ZOOM"
+STATIC = "STATIC"
SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK = "SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK"
SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION = (
"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION"
@@ -36,3 +37,5 @@
IGNORE = "IGNORE"
PROOF_OF_ADDRESS = "PROOF_OF_ADDRESS"
+
+WATCHLIST_SCREENING_CHECK_TYPE = "WATCHLIST_SCREENING"
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 c0c38292..89d5689f 100644
--- a/yoti_python_sdk/doc_scan/session/create/check/__init__.py
+++ b/yoti_python_sdk/doc_scan/session/create/check/__init__.py
@@ -2,10 +2,13 @@
from .document_comparison import RequestedIDDocumentComparisonCheckBuilder
from .face_match import RequestedFaceMatchCheckBuilder
from .liveness import RequestedLivenessCheckBuilder
+from .watchlist_screen import WatchlistScreeningCheckBuilder
+
__all__ = [
"RequestedDocumentAuthenticityCheckBuilder",
"RequestedIDDocumentComparisonCheckBuilder",
"RequestedFaceMatchCheckBuilder",
"RequestedLivenessCheckBuilder",
+ "WatchlistScreeningCheckBuilder",
]
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 f9dc1075..57ae8e4f 100644
--- a/yoti_python_sdk/doc_scan/session/create/check/liveness.py
+++ b/yoti_python_sdk/doc_scan/session/create/check/liveness.py
@@ -11,15 +11,18 @@ class RequestedLivenessCheckConfig(YotiSerializable):
The configuration applied when creating a Liveness Check
"""
- def __init__(self, liveness_type, max_retries):
+ def __init__(self, liveness_type, max_retries, manual_check=None):
"""
:param liveness_type: the liveness type
:type liveness_type: str
:param max_retries: the maximum number of retries
:type max_retries: int
+ :param manual_check: the manual check value
+ :type manual_check: str or None
"""
self.__liveness_type = liveness_type
self.__max_retries = max_retries
+ self.__manual_check = manual_check
@property
def liveness_type(self):
@@ -39,9 +42,23 @@ def max_retries(self):
"""
return self.__max_retries
+ @property
+ def manual_check(self):
+ """
+ The manual check value for the liveness check
+
+ :return: the manual check value
+ :rtype: str or None
+ """
+ return self.__manual_check
+
def to_json(self):
return remove_null_values(
- {"liveness_type": self.liveness_type, "max_retries": self.max_retries}
+ {
+ "liveness_type": self.liveness_type,
+ "max_retries": self.max_retries,
+ "manual_check": self.manual_check,
+ }
)
@@ -74,6 +91,7 @@ class RequestedLivenessCheckBuilder(object):
def __init__(self):
self.__liveness_type = None
self.__max_retries = None
+ self.__manual_check = None
def for_zoom_liveness(self):
"""
@@ -84,6 +102,15 @@ def for_zoom_liveness(self):
"""
return self.with_liveness_type(constants.ZOOM)
+ def for_static_liveness(self):
+ """
+ Sets the liveness type to "STATIC"
+
+ :return: the builder
+ :rtype: RequestedLivenessCheckBuilder
+ """
+ return self.with_liveness_type(constants.STATIC)
+
def with_liveness_type(self, liveness_type):
"""
Sets the liveness type on the builder
@@ -109,6 +136,18 @@ def with_max_retries(self, max_retries):
self.__max_retries = max_retries
return self
+ def with_manual_check_never(self):
+ """
+ Sets the manual check value to "NEVER"
+
+ :return: the builder
+ :rtype: RequestedLivenessCheckBuilder
+ """
+ self.__manual_check = constants.NEVER
+ return self
+
def build(self):
- config = RequestedLivenessCheckConfig(self.__liveness_type, self.__max_retries)
+ config = RequestedLivenessCheckConfig(
+ self.__liveness_type, self.__max_retries, self.__manual_check
+ )
return RequestedLivenessCheck(config)
diff --git a/yoti_python_sdk/doc_scan/session/create/check/watchlist_screen.py b/yoti_python_sdk/doc_scan/session/create/check/watchlist_screen.py
new file mode 100644
index 00000000..8151abcf
--- /dev/null
+++ b/yoti_python_sdk/doc_scan/session/create/check/watchlist_screen.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from yoti_python_sdk.doc_scan import constants
+from yoti_python_sdk.utils import YotiSerializable, remove_null_values
+from .requested_check import RequestedCheck
+
+
+class WatchlistScreeningCheckConfig(YotiSerializable):
+ """
+ The configuration applied when creating a Watchlist screening check.
+ """
+
+ def __init__(self, manual_check, categories):
+ """
+ :param manual_check: the watchlist screening check manual_check eg. "NEVER"
+ :type type: str
+ :param categories: list of categories for watchlist screening check config
+ :type max_retries: list
+ """
+ self.__categories = categories
+ self.__manual_check = manual_check
+
+ @property
+ def manual_check(self):
+ """
+ Watchlist screening check manual check value
+
+ :return: str
+ """
+ return self.__manual_check
+
+ @property
+ def categories(self):
+ """
+ Watchlist screening check categories
+
+ :return: list
+ """
+ return self.__categories
+
+ def to_json(self):
+ return remove_null_values(
+ {"manual_check": self.manual_check, "categories": self.__categories}
+ )
+
+
+class WatchlistScreeningCheck(RequestedCheck):
+ """
+ Requests creation of a Watchlist screening check
+ """
+
+ def __init__(self, config):
+ """
+ :param config: the Watchlist screening check configuration
+ :type config: WatchlistScreeningCheckConfig
+ """
+ self.__config = config
+
+ @property
+ def type(self):
+ return constants.WATCHLIST_SCREENING_CHECK_TYPE
+
+ @property
+ def config(self):
+ return self.__config
+
+
+class WatchlistScreeningCheckBuilder(object):
+ """
+ Builder to assist creation of :class:`WatchlistScreeningCheck`
+ """
+
+ def __init__(self):
+ self.__categories = None
+ self.__manual_check = None
+
+ def with_categories(self, categories):
+ """
+ Sets the WatchListScreeningCheck categories
+
+ :return: the builder
+ :rtype: WatchlistScreeningCheckBuilder
+ """
+ self.__categories = categories
+
+ return self
+
+ def with_manual_check(self, manual_check):
+ """
+ Sets the WatchListScreeningCheck manual_check
+
+ :param liveness_type: the manual_check
+ :type liveness_type: str
+ :return: the builder
+ :rtype: WatchlistScreeningCheckBuilder
+ """
+ self.__manual_check = manual_check
+
+ return self
+
+ def build(self):
+ config = WatchlistScreeningCheckConfig(self.__manual_check, self.__categories or [])
+ return WatchlistScreeningCheck(config)
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 6e0bcf4e..c86eca3b 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
@@ -68,11 +68,12 @@ def to_json(self):
class OrthogonalRestrictionsFilter(DocumentFilter):
- def __init__(self, country_restriction, type_restriction):
+ def __init__(self, country_restriction, type_restriction, allow_non_latin_documents=None):
DocumentFilter.__init__(self, filter_type=ORTHOGONAL_RESTRICTIONS)
self.__country_restriction = country_restriction
self.__type_restriction = type_restriction
+ self.__allow_non_latin_documents = allow_non_latin_documents
@property
def country_restriction(self):
@@ -94,10 +95,23 @@ def type_restriction(self):
"""
return self.__type_restriction
+ @property
+ def allow_non_latin_documents(self):
+ """
+ Returns the flag for whether non-latin documents are allowed.
+
+ :return: allow_non_latin_documents
+ :rtype: bool
+ """
+ return self.__allow_non_latin_documents
+
def to_json(self):
parent = DocumentFilter.to_json(self)
parent["country_restriction"] = self.country_restriction
parent["type_restriction"] = self.type_restriction
+ if self.__allow_non_latin_documents is not None:
+ parent["allow_non_latin_documents"] = self.__allow_non_latin_documents
+
return remove_null_values(parent)
@@ -117,6 +131,7 @@ class OrthogonalRestrictionsFilterBuilder(object):
def __init__(self):
self.__country_restriction = None
self.__type_restriction = None
+ self.__allow_non_latin_documents = None
def with_whitelisted_country_codes(self, country_codes):
"""
@@ -170,6 +185,26 @@ def with_blacklisted_document_types(self, document_types):
self.__type_restriction = TypeRestriction(INCLUSION_BLACKLIST, document_types)
return self
+ def allow_non_latin_documents(self):
+ """
+ Sets a True value for "allow non-latin documents" flag.
+
+ :return: the builder
+ :rtype: OrthogonalRestrictionsFilterBuilder
+ """
+ self.__allow_non_latin_documents = True
+ return self
+
+ def disable_non_latin_documents(self):
+ """
+ Sets a False value for "allow non-latin documents" flag.
+
+ :return: the builder
+ :rtype: OrthogonalRestrictionsFilterBuilder
+ """
+ self.__allow_non_latin_documents = False
+ return self
+
def build(self):
"""
Builds the orthogonal filter, using the supplied whitelisted/blacklisted values
@@ -178,5 +213,5 @@ def build(self):
:rtype: OrthogonalRestrictionsFilter
"""
return OrthogonalRestrictionsFilter(
- self.__country_restriction, self.__type_restriction
+ self.__country_restriction, self.__type_restriction, self.__allow_non_latin_documents
)
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 476a38b0..af68656e 100644
--- a/yoti_python_sdk/doc_scan/session/create/notification_config.py
+++ b/yoti_python_sdk/doc_scan/session/create/notification_config.py
@@ -16,7 +16,7 @@ class NotificationConfig(YotiSerializable):
to poll for the state of the Session.
"""
- def __init__(self, auth_token, endpoint, topics=None):
+ def __init__(self, auth_token, endpoint, topics=None, auth_type=None):
"""
:param auth_token: the authorization token
:type auth_token: str
@@ -31,6 +31,7 @@ def __init__(self, auth_token, endpoint, topics=None):
self.__auth_token = auth_token
self.__endpoint = endpoint
self.__topics = list(set(topics)) # Get unique values
+ self.__auth_type = auth_type
@property
def auth_token(self):
@@ -62,9 +63,21 @@ def topics(self):
"""
return self.__topics
+ @property
+ def auth_type(self):
+ """
+ The authentication type that the notification will use to
+ authenticate itself.
+
+ :return: the endpoint
+ :rtype: str
+ """
+ return self.__auth_type
+
def to_json(self):
return remove_null_values(
{
+ "auth_type": self.auth_type,
"auth_token": self.auth_token,
"endpoint": self.endpoint,
"topics": self.topics,
@@ -81,6 +94,7 @@ def __init__(self):
self.__auth_token = None
self.__endpoint = None
self.__topics = []
+ self.__auth_type = None
def with_auth_token(self, token):
"""
@@ -154,6 +168,26 @@ def for_check_completion(self):
"""
return self.with_topic(CHECK_COMPLETION)
+ def with_basic_auth_type(self):
+ """
+ Setup "BASIC" auth type for notifications.
+
+ :return: the builder
+ :rtype: NotificationConfigBuilder
+ """
+ self.__auth_type = "BASIC"
+ return self
+
+ def with_bearer_auth_type(self):
+ """
+ Setup "BEARER" auth type for notifications.
+
+ :return: the builder
+ :rtype: NotificationConfigBuilder
+ """
+ self.__auth_type = "BEARER"
+ return self
+
def build(self):
"""
Builds the :class:`NotificationConfig` using the supplied values
@@ -161,4 +195,4 @@ def build(self):
:return: the build notification config
:rtype: NotificationConfig
"""
- return NotificationConfig(self.__auth_token, self.__endpoint, self.__topics)
+ return NotificationConfig(self.__auth_token, self.__endpoint, self.__topics, self.__auth_type)
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 c76fe897..37cde6b5 100644
--- a/yoti_python_sdk/doc_scan/session/create/sdk_config.py
+++ b/yoti_python_sdk/doc_scan/session/create/sdk_config.py
@@ -21,6 +21,8 @@ def __init__(
preset_issuing_country,
success_url,
error_url,
+ allow_handoff=None,
+ privacy_policy_url=None,
):
"""
:param allowed_capture_methods: the allowed capture methods
@@ -39,6 +41,10 @@ def __init__(
:type success_url: str
:param error_url: the error url
:type error_url: str
+ :param privacy_policy_url: the privacy policy url
+ :type privacy_policy_url: str
+ :param allow_handoff: boolean flag for allow_handoff
+ :type allow_handoff: bool
"""
self.__allowed_capture_methods = allowed_capture_methods
self.__primary_colour = primary_colour
@@ -48,6 +54,8 @@ def __init__(
self.__preset_issuing_country = preset_issuing_country
self.__success_url = success_url
self.__error_url = error_url
+ self.__privacy_policy_url = privacy_policy_url
+ self.__allow_handoff = allow_handoff
@property
def allowed_capture_methods(self):
@@ -121,6 +129,25 @@ def error_url(self):
"""
return self.__error_url
+ @property
+ def privacy_policy_url(self):
+ """
+ The privacy policy URL
+
+ :return: the privacy policy url
+ """
+ return self.__privacy_policy_url
+
+ @property
+ def allow_handoff(self):
+ """
+ Flag to enable/disable relying business to handoff
+ support when creating a session.
+
+ :return: the allow_handoff
+ """
+ return self.__allow_handoff
+
def to_json(self):
return remove_null_values(
{
@@ -132,6 +159,8 @@ def to_json(self):
"preset_issuing_country": self.preset_issuing_country,
"success_url": self.success_url,
"error_url": self.error_url,
+ "privacy_policy_url": self.privacy_policy_url,
+ "allow_handoff": self.allow_handoff,
}
)
@@ -150,6 +179,8 @@ def __init__(self):
self.__preset_issuing_country = None
self.__success_url = None
self.__error_url = None
+ self.__privacy_policy_url = None
+ self.__allow_handoff = None
def with_allowed_capture_methods(self, allowed_capture_methods):
"""
@@ -265,6 +296,30 @@ def with_error_url(self, url):
self.__error_url = url
return self
+ def with_privacy_policy_url(self, url):
+ """
+ Sets the privacy policy URL
+
+ :param url: the privacy policy URL
+ :type url: str
+ :return: the builder
+ :rtype: SdkConfigBuilder
+ """
+ self.__privacy_policy_url = url
+ return self
+
+ def with_allow_handoff(self, flag):
+ """
+ Sets the allow handoff flag
+
+ :param flag: boolean value for flag
+ :type flag: bool
+ :return: the builder
+ :rtype: SdkConfigBuilder
+ """
+ self.__allow_handoff = flag
+ return self
+
def build(self):
return SdkConfig(
self.__allowed_capture_methods,
@@ -275,4 +330,6 @@ def build(self):
self.__preset_issuing_country,
self.__success_url,
self.__error_url,
+ self.__allow_handoff,
+ self.__privacy_policy_url,
)
diff --git a/yoti_python_sdk/doc_scan/session/create/session_spec.py b/yoti_python_sdk/doc_scan/session/create/session_spec.py
index ef3bbce4..75a2b8be 100644
--- a/yoti_python_sdk/doc_scan/session/create/session_spec.py
+++ b/yoti_python_sdk/doc_scan/session/create/session_spec.py
@@ -21,6 +21,7 @@ def __init__(
requested_tasks=None,
required_documents=None,
block_biometric_consent=None,
+ session_deadline=None,
):
"""
:param client_session_token_ttl: the client session token TTL
@@ -41,6 +42,8 @@ def __init__(
:type required_documents: list[RequiredDocument] or None
:param block_biometric_consent: block the collection of biometric consent
:type block_biometric_consent: bool
+ :param session_deadline: session deadline using a Zoned timestamp
+ "type session_deadline: str
"""
if requested_tasks is None:
requested_tasks = []
@@ -58,6 +61,7 @@ def __init__(
self.__requested_tasks = requested_tasks
self.__required_documents = required_documents
self.__block_biometric_consent = block_biometric_consent
+ self.__session_deadline = session_deadline
@property
def client_session_token_ttl(self):
@@ -152,6 +156,16 @@ def block_biometric_consent(self):
"""
return self.__block_biometric_consent
+ @property
+ def session_deadline(self):
+ """
+ Session deadline used by IDV
+
+ :return: session deadline
+ :rtype: str
+ """
+ return self.__session_deadline
+
def to_json(self):
return remove_null_values(
{
@@ -164,6 +178,7 @@ def to_json(self):
"sdk_config": self.sdk_config,
"required_documents": self.required_documents,
"block_biometric_consent": self.block_biometric_consent,
+ "session_deadline": self.session_deadline,
}
)
@@ -183,6 +198,7 @@ def __init__(self):
self.__requested_tasks = []
self.__required_documents = []
self.__block_biometric_consent = None
+ self.__session_deadline = None
def with_client_session_token_ttl(self, value):
"""
@@ -196,6 +212,19 @@ def with_client_session_token_ttl(self, value):
self.__client_session_token_ttl = value
return self
+ def with_session_deadline(self, value):
+ """
+ Sets the deadline that the session needs to be completed by.
+ Can be used as an alternative to with_client_session_token_ttl.
+
+ :param value: the session deadline
+ :type value: str
+ :return: the builder
+ :rtype: SessionSpecBuilder
+ """
+ self.__session_deadline = value
+ return self
+
def with_resources_ttl(self, value):
"""
Sets the resources TTL (time-to-live)
@@ -309,4 +338,5 @@ def build(self):
self.__requested_tasks,
self.__required_documents,
self.__block_biometric_consent,
+ self.__session_deadline,
)
diff --git a/yoti_python_sdk/doc_scan/session/create/subcheck/__init__.py b/yoti_python_sdk/doc_scan/session/create/subcheck/__init__.py
new file mode 100644
index 00000000..2e248d33
--- /dev/null
+++ b/yoti_python_sdk/doc_scan/session/create/subcheck/__init__.py
@@ -0,0 +1,5 @@
+from .issuing_authority_sub_check import IssuingAuthoritySubCheckBuilder
+
+__all__ = [
+ "IssuingAuthoritySubCheckBuilder",
+]
diff --git a/yoti_python_sdk/doc_scan/session/create/subcheck/issuing_authority_sub_check.py b/yoti_python_sdk/doc_scan/session/create/subcheck/issuing_authority_sub_check.py
new file mode 100644
index 00000000..376171b7
--- /dev/null
+++ b/yoti_python_sdk/doc_scan/session/create/subcheck/issuing_authority_sub_check.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+
+from yoti_python_sdk.doc_scan.session.create.filter.document_filter import DocumentFilter
+from yoti_python_sdk.utils import YotiSerializable
+from .sub_check import SubRequestedCheck
+
+
+class IssuingAuthoritySubCheck(SubRequestedCheck):
+ """
+ Requests creation of an Issuing Authority Sub Check.
+ """
+
+ def __init__(self, filter=None):
+ self._filter = filter
+
+ @property
+ def requested(self):
+ return True
+
+ @property
+ def filter(self):
+ return self._filter
+
+
+class IssuingAuthoritySubCheckBuilder:
+ """
+ Builder for Issuing Authority Sub Check.
+ """
+ def __init__(self):
+ self._filter = None
+
+ def with_filter(self, filter):
+ if not issubclass(type(filter), DocumentFilter):
+ raise ValueError('invalid filter')
+
+ self._filter = filter
+
+ return self
+
+ def build(self):
+ return IssuingAuthoritySubCheck(filter=self._filter)
diff --git a/yoti_python_sdk/doc_scan/session/create/subcheck/sub_check.py b/yoti_python_sdk/doc_scan/session/create/subcheck/sub_check.py
new file mode 100644
index 00000000..d4fa882d
--- /dev/null
+++ b/yoti_python_sdk/doc_scan/session/create/subcheck/sub_check.py
@@ -0,0 +1,20 @@
+from abc import ABCMeta
+from abc import abstractmethod
+
+from yoti_python_sdk.utils import YotiSerializable
+
+
+class SubRequestedCheck(YotiSerializable):
+ """
+ Requests creation of a SubCheck to be performed on a document
+ """
+
+ __metaclass__ = ABCMeta
+
+ @property
+ @abstractmethod
+ def type(self):
+ raise NotImplementedError
+
+ def to_json(self):
+ return remove_null_values({"type": self.type, "config": self.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 f1cb515d..f05c4cf5 100644
--- a/yoti_python_sdk/doc_scan/session/retrieve/check_response.py
+++ b/yoti_python_sdk/doc_scan/session/retrieve/check_response.py
@@ -184,3 +184,11 @@ class SupplementaryDocumentTextDataCheckResponse(CheckResponse):
"""
pass
+
+
+class WatchlistScreeningCheckResponse(CheckResponse):
+ """
+ Represents a watchlist screening check for a given session
+ """
+
+ pass
diff --git a/yoti_python_sdk/doc_scan/session/retrieve/create_session_result.py b/yoti_python_sdk/doc_scan/session/retrieve/create_session_result.py
index c64dd9da..2990f473 100644
--- a/yoti_python_sdk/doc_scan/session/retrieve/create_session_result.py
+++ b/yoti_python_sdk/doc_scan/session/retrieve/create_session_result.py
@@ -16,6 +16,7 @@ def __init__(self, data=None):
data = dict()
self.__client_session_token_ttl = data.get("client_session_token_ttl", None)
+ self.__session_deadline = data.get("session_deadline", None)
self.__session_id = data.get("session_id", None)
self.__client_session_token = data.get("client_session_token", None)
@@ -49,3 +50,13 @@ def session_id(self):
:rtype: str or None
"""
return self.__session_id
+
+ @property
+ def session_deadline(self):
+ """
+ The deadline that the session needs to be completed by.
+
+ :return: the session deadline
+ :rtype: str or None
+ """
+ return self.__session_deadline
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 f39eed98..7f238281 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
@@ -16,6 +16,7 @@
LivenessCheckResponse,
TextDataCheckResponse,
SupplementaryDocumentTextDataCheckResponse,
+ WatchlistScreeningCheckResponse,
)
from .resource_container import ResourceContainer
@@ -80,6 +81,7 @@ def __parse_check(check):
constants.ID_DOCUMENT_FACE_MATCH: FaceMatchCheckResponse,
constants.ID_DOCUMENT_TEXT_DATA_CHECK: TextDataCheckResponse,
constants.LIVENESS: LivenessCheckResponse,
+ constants.WATCHLIST_SCREENING_CHECK_TYPE: WatchlistScreeningCheckResponse,
constants.ID_DOCUMENT_COMPARISON: IDDocumentComparisonCheckResponse,
constants.SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK: SupplementaryDocumentTextDataCheckResponse,
}
diff --git a/yoti_python_sdk/doc_scan/session/retrieve/image_response.py b/yoti_python_sdk/doc_scan/session/retrieve/image_response.py
new file mode 100644
index 00000000..f983e550
--- /dev/null
+++ b/yoti_python_sdk/doc_scan/session/retrieve/image_response.py
@@ -0,0 +1,32 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .media_response import MediaResponse
+
+
+class ImageResponse(object):
+ """
+ Represents an image resource within a static liveness check
+ """
+
+ def __init__(self, data=None):
+ """
+ :param data: the data to parse
+ :type data: dict or None
+ """
+ if data is None:
+ data = dict()
+
+ self.__media = (
+ MediaResponse(data["media"]) if "media" in data.keys() else None
+ )
+
+ @property
+ def media(self):
+ """
+ Returns the media information for the image
+
+ :return: the media
+ :rtype: MediaResponse or None
+ """
+ return self.__media
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 8295e503..3aaf0ecd 100644
--- a/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py
+++ b/yoti_python_sdk/doc_scan/session/retrieve/resource_container.py
@@ -11,6 +11,9 @@
LivenessResourceResponse,
ZoomLivenessResourceResponse,
)
+from yoti_python_sdk.doc_scan.session.retrieve.static_liveness_resource_response import (
+ StaticLivenessResourceResponse,
+)
class ResourceContainer(object):
@@ -54,7 +57,7 @@ def __parse_liveness_capture(liveness_capture):
:return: the parsed liveness capture
:rtype: LivenessResourceResponse
"""
- types = {"ZOOM": ZoomLivenessResourceResponse}
+ types = {"ZOOM": ZoomLivenessResourceResponse, "STATIC": StaticLivenessResourceResponse}
clazz = types.get(
liveness_capture.get("liveness_type", None),
@@ -105,3 +108,17 @@ def zoom_liveness_resources(self):
for liveness in self.__liveness_capture
if isinstance(liveness, ZoomLivenessResourceResponse)
]
+
+ @property
+ def static_liveness_resources(self):
+ """
+ Returns a filtered list of static liveness capture resources
+
+ :return: list of static liveness captures
+ :rtype: list[StaticLivenessResourceResponse]
+ """
+ return [
+ liveness
+ for liveness in self.__liveness_capture
+ if isinstance(liveness, StaticLivenessResourceResponse)
+ ]
diff --git a/yoti_python_sdk/doc_scan/session/retrieve/static_liveness_resource_response.py b/yoti_python_sdk/doc_scan/session/retrieve/static_liveness_resource_response.py
new file mode 100644
index 00000000..970258e1
--- /dev/null
+++ b/yoti_python_sdk/doc_scan/session/retrieve/static_liveness_resource_response.py
@@ -0,0 +1,35 @@
+# -*- coding: utf-8 -*-
+from __future__ import unicode_literals
+
+from .liveness_resource_response import LivenessResourceResponse
+from .image_response import ImageResponse
+
+
+class StaticLivenessResourceResponse(LivenessResourceResponse):
+ """
+ Represents a Static Liveness resource for a given session
+ """
+
+ def __init__(self, data=None):
+ """
+ :param data: the data to parse
+ :type data: dict or None
+ """
+ if data is None:
+ data = dict()
+
+ LivenessResourceResponse.__init__(self, data)
+
+ self.__image = (
+ ImageResponse(data["image"]) if "image" in data.keys() else None
+ )
+
+ @property
+ def image(self):
+ """
+ Returns the associated image for the static liveness resource
+
+ :return: the image
+ :rtype: ImageResponse or None
+ """
+ return self.__image
diff --git a/yoti_python_sdk/doc_scan/support/supported_documents.py b/yoti_python_sdk/doc_scan/support/supported_documents.py
index 55505b78..a35a21e1 100644
--- a/yoti_python_sdk/doc_scan/support/supported_documents.py
+++ b/yoti_python_sdk/doc_scan/support/supported_documents.py
@@ -4,11 +4,16 @@ def __init__(self, data=None):
data = dict()
self.__type = data.get("type", None)
+ self.__is_strictly_latin = data.get("is_strictly_latin", None)
@property
def type(self):
return self.__type
+ @property
+ def is_strictly_latin(self):
+ return self.__is_strictly_latin
+
class SupportedCountry(object):
def __init__(self, data=None):
diff --git a/yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py b/yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py
index 130deb69..6b14f934 100644
--- a/yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py
+++ b/yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py
@@ -52,6 +52,67 @@ 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_static_liveness_type(self):
+ result = (
+ RequestedLivenessCheckBuilder()
+ .for_static_liveness()
+ .with_max_retries(3)
+ .build()
+ )
+
+ assert result.type == "LIVENESS"
+ assert result.config.liveness_type == "STATIC"
+ assert result.config.max_retries == 3
+
+ def test_should_build_with_manual_check_never(self):
+ result = (
+ RequestedLivenessCheckBuilder()
+ .for_static_liveness()
+ .with_max_retries(3)
+ .with_manual_check_never()
+ .build()
+ )
+
+ assert result.config.liveness_type == "STATIC"
+ assert result.config.manual_check == "NEVER"
+
+ def test_should_serialize_static_liveness_to_json(self):
+ result = (
+ RequestedLivenessCheckBuilder()
+ .for_static_liveness()
+ .with_max_retries(3)
+ .with_manual_check_never()
+ .build()
+ )
+
+ json_str = json.dumps(result, cls=YotiEncoder)
+ assert json_str is not None
+
+ # Verify the JSON contains the expected fields
+ json_data = json.loads(json_str)
+ assert json_data["type"] == "LIVENESS"
+ assert json_data["config"]["liveness_type"] == "STATIC"
+ assert json_data["config"]["manual_check"] == "NEVER"
+ assert json_data["config"]["max_retries"] == 3
+
+ def test_should_omit_manual_check_when_not_set(self):
+ result = (
+ RequestedLivenessCheckBuilder()
+ .for_static_liveness()
+ .with_max_retries(3)
+ .build()
+ )
+
+ json_str = json.dumps(result, cls=YotiEncoder)
+ assert json_str is not None
+
+ # Verify the JSON does not contain the manual_check field
+ json_data = json.loads(json_str)
+ assert json_data["type"] == "LIVENESS"
+ assert json_data["config"]["liveness_type"] == "STATIC"
+ assert "manual_check" not in json_data["config"]
+ assert json_data["config"]["max_retries"] == 3
+
if __name__ == "__main__":
unittest.main()
diff --git a/yoti_python_sdk/tests/doc_scan/session/create/check/test_wathclist_check.py b/yoti_python_sdk/tests/doc_scan/session/create/check/test_wathclist_check.py
new file mode 100644
index 00000000..95d04539
--- /dev/null
+++ b/yoti_python_sdk/tests/doc_scan/session/create/check/test_wathclist_check.py
@@ -0,0 +1,106 @@
+import json
+import unittest
+
+from yoti_python_sdk.doc_scan import constants
+from yoti_python_sdk.doc_scan.session.create.check import (
+ WatchlistScreeningCheckBuilder,
+)
+from yoti_python_sdk.doc_scan.session.create.check.requested_check import RequestedCheck
+from yoti_python_sdk.doc_scan.session.create.check.watchlist_screen import (
+ WatchlistScreeningCheck,
+ WatchlistScreeningCheckConfig,
+)
+from yoti_python_sdk.utils import YotiEncoder
+from yoti_python_sdk.doc_scan.constants import WATCHLIST_SCREENING_CHECK_TYPE
+
+
+class WatchlistScreeningCheckTest(unittest.TestCase):
+ def test_should_build_correctly_with_manual_check(self):
+ dummy_manual_check = "DUMMY_VALUE"
+
+ result = (
+ WatchlistScreeningCheckBuilder()
+ .with_manual_check(dummy_manual_check)
+ .build()
+ )
+
+ assert isinstance(result, RequestedCheck)
+ assert isinstance(result, WatchlistScreeningCheck)
+
+ assert result.type == constants.WATCHLIST_SCREENING_CHECK_TYPE
+ assert result.config.manual_check == dummy_manual_check
+ assert result.config.categories == []
+
+ def test_should_build_corretly_with_categories(self):
+ dummy_categories = ["FIRST", "SECOND"]
+
+ result = (
+ WatchlistScreeningCheckBuilder()
+ .with_categories(dummy_categories)
+ .build()
+ )
+
+ assert isinstance(result, RequestedCheck)
+ assert isinstance(result, WatchlistScreeningCheck)
+
+ assert result.type == constants.WATCHLIST_SCREENING_CHECK_TYPE
+ assert result.config.categories == dummy_categories
+ assert result.config.manual_check is None
+
+ def test_should_build_correctly_with_manual_check_and_categories(self):
+ dummy_manual_check = "DUMMY_VALUE"
+ dummy_categories = ["FIRST", "SECOND"]
+
+ result = (
+ WatchlistScreeningCheckBuilder()
+ .with_manual_check(dummy_manual_check)
+ .with_categories(dummy_categories)
+ .build()
+ )
+
+ assert isinstance(result, RequestedCheck)
+ assert isinstance(result, WatchlistScreeningCheck)
+
+ assert result.type == constants.WATCHLIST_SCREENING_CHECK_TYPE
+ assert result.config.manual_check == dummy_manual_check
+ assert result.config.categories == dummy_categories
+
+ def test_should_serialize_to_json_without_error(self):
+ another_dummy_manual_check = "DUMMY_VALUE"
+ another_dummy_categories = ["FIRST", "SECOND"]
+
+ result = (
+ WatchlistScreeningCheckBuilder()
+ .with_manual_check(another_dummy_manual_check)
+ .with_categories(another_dummy_categories)
+ .build()
+ )
+
+ s = json.dumps(result, cls=YotiEncoder)
+ assert s is not None and s != ""
+
+ result = (
+ WatchlistScreeningCheckBuilder()
+ .with_categories(another_dummy_categories)
+ .build()
+ )
+
+ s = json.dumps(result, cls=YotiEncoder)
+ assert s is not None and s != ""
+
+ s = json.loads(s)
+ assert s.get("type") == WATCHLIST_SCREENING_CHECK_TYPE
+ assert s.get("config") == {"categories": another_dummy_categories}
+
+ result = (
+ WatchlistScreeningCheckBuilder()
+ .with_manual_check(another_dummy_manual_check)
+ .build()
+ )
+
+ s = json.dumps(result, cls=YotiEncoder)
+ assert s is not None and s != ""
+
+ s = json.loads(s)
+ assert s.get("type") == WATCHLIST_SCREENING_CHECK_TYPE
+ assert s.get("config") == {"manual_check": "DUMMY_VALUE", "categories": []}
diff --git a/yoti_python_sdk/tests/doc_scan/session/create/subcheck/__init__.py b/yoti_python_sdk/tests/doc_scan/session/create/subcheck/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/yoti_python_sdk/tests/doc_scan/session/create/subcheck/test_issuing_authority_sub_check.py b/yoti_python_sdk/tests/doc_scan/session/create/subcheck/test_issuing_authority_sub_check.py
new file mode 100644
index 00000000..083ea1a0
--- /dev/null
+++ b/yoti_python_sdk/tests/doc_scan/session/create/subcheck/test_issuing_authority_sub_check.py
@@ -0,0 +1,56 @@
+import unittest
+
+from yoti_python_sdk.doc_scan.session.create.filter.orthogonal_restrictions_filter import \
+ OrthogonalRestrictionsFilterBuilder
+from yoti_python_sdk.doc_scan.session.create.subcheck import (
+ IssuingAuthoritySubCheckBuilder
+)
+from yoti_python_sdk.doc_scan.session.create.subcheck.issuing_authority_sub_check import IssuingAuthoritySubCheck
+
+
+class IssuingAuthoritySubCheckTest(unittest.TestCase):
+ def test_should_build_correctly_without_additional_data(self):
+ issuing_authority_sub_check = IssuingAuthoritySubCheckBuilder().build()
+
+ assert isinstance(issuing_authority_sub_check, IssuingAuthoritySubCheck)
+
+ def test_should_build_correctly_with_filter(self):
+ filter = OrthogonalRestrictionsFilterBuilder().with_whitelisted_country_codes(
+ ["GBR", "FRA"]).with_whitelisted_document_types(["PASSPORT", "STATE_ID"]).build()
+
+ issuing_authority_sub_check = IssuingAuthoritySubCheckBuilder().with_filter(
+ filter=filter).build()
+
+ assert isinstance(issuing_authority_sub_check, IssuingAuthoritySubCheck)
+ assert issuing_authority_sub_check.filter == filter
+
+ def test_should_always_build_with_requested_as_boolean_true(self):
+ issuing_authority_sub_check = IssuingAuthoritySubCheckBuilder().build()
+
+ assert issuing_authority_sub_check.requested is True
+
+ def test_allow_non_latin_documents_set_to_true(self):
+ filter = OrthogonalRestrictionsFilterBuilder().allow_non_latin_documents().build()
+
+ assert filter.allow_non_latin_documents is True
+
+ def test_allow_non_latin_documents_set_to_false(self):
+ filter = OrthogonalRestrictionsFilterBuilder().disable_non_latin_documents().build()
+
+ assert filter.allow_non_latin_documents is False
+
+ def test_default_non_latin_documents(self):
+ filter = OrthogonalRestrictionsFilterBuilder().build()
+
+ assert 'allow_non_latin_documents' not in filter.to_json()
+
+ def test_build_invalid_filter(self):
+ filter = 'invalid'
+
+ with self.assertRaises(ValueError):
+ IssuingAuthoritySubCheckBuilder().with_filter(
+ filter=filter).build()
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/yoti_python_sdk/tests/doc_scan/session/create/test_notification_config.py b/yoti_python_sdk/tests/doc_scan/session/create/test_notification_config.py
index f207ff14..1fa09b29 100644
--- a/yoti_python_sdk/tests/doc_scan/session/create/test_notification_config.py
+++ b/yoti_python_sdk/tests/doc_scan/session/create/test_notification_config.py
@@ -12,6 +12,7 @@ class NotificationConfigTest(unittest.TestCase):
SOME_AUTH_TOKEN = "someAuthToken"
SOME_ENDPOINT = "someEndpoint"
SOME_TOPIC = "someTopic"
+ SOME_AUTH_TYPE = "someAuthType"
def test_should_build_correctly(self):
result = (
@@ -26,6 +27,7 @@ def test_should_build_correctly(self):
assert result.auth_token is self.SOME_AUTH_TOKEN
assert result.endpoint is self.SOME_ENDPOINT
assert self.SOME_TOPIC in result.topics
+ assert result.auth_type is None
def test_should_add_resource_update_topic(self):
result = (
@@ -104,6 +106,30 @@ def test_should_store_unique_topics(self):
assert len(result.topics) == 1
+ def test_build_with_basic_auth_type(self):
+ result = (
+ NotificationConfigBuilder()
+ .with_auth_token(self.SOME_AUTH_TOKEN)
+ .with_endpoint(self.SOME_ENDPOINT)
+ .with_topic(self.SOME_TOPIC)
+ .with_basic_auth_type()
+ .build()
+ )
+
+ assert result.auth_type is "BASIC"
+
+ def test_build_with_bearer_auth_type(self):
+ result = (
+ NotificationConfigBuilder()
+ .with_auth_token(self.SOME_AUTH_TOKEN)
+ .with_endpoint(self.SOME_ENDPOINT)
+ .with_topic(self.SOME_TOPIC)
+ .with_bearer_auth_type()
+ .build()
+ )
+
+ assert result.auth_type is "BEARER"
+
def test_should_serialize_to_json_without_error(self):
result = (
NotificationConfigBuilder()
diff --git a/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py b/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py
index 6de7b4ca..d621a441 100644
--- a/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py
+++ b/yoti_python_sdk/tests/doc_scan/session/create/test_sdk_config.py
@@ -14,6 +14,8 @@ class SdkConfigTest(unittest.TestCase):
SOME_PRESET_ISSUING_COUNTRY = "USA"
SOME_SUCCESS_URL = "https://mysite.com/yoti/success"
SOME_ERROR_URL = "https://mysite.com/yoti/error"
+ SOME_PRIVACY_POLICY_URL = "https://mysite.com/privacy"
+ SOME_ALLOW_HANDOFF = True
def test_should_build_correctly(self):
result = (
@@ -26,6 +28,8 @@ def test_should_build_correctly(self):
.with_preset_issuing_country(self.SOME_PRESET_ISSUING_COUNTRY)
.with_success_url(self.SOME_SUCCESS_URL)
.with_error_url(self.SOME_ERROR_URL)
+ .with_privacy_policy_url(self.SOME_PRIVACY_POLICY_URL)
+ .with_allow_handoff(self.SOME_ALLOW_HANDOFF)
.build()
)
@@ -38,12 +42,24 @@ def test_should_build_correctly(self):
assert result.preset_issuing_country is self.SOME_PRESET_ISSUING_COUNTRY
assert result.success_url is self.SOME_SUCCESS_URL
assert result.error_url is self.SOME_ERROR_URL
+ assert result.privacy_policy_url is self.SOME_PRIVACY_POLICY_URL
+ assert result.allow_handoff is True
def test_should_allows_camera(self):
result = SdkConfigBuilder().with_allows_camera().build()
assert result.allowed_capture_methods == "CAMERA"
+ def test_not_passing_allow_handoff(self):
+ result = SdkConfigBuilder().with_allows_camera().build()
+
+ assert result.allow_handoff is None
+
+ def test_passing_allow_handoff_false_value(self):
+ result = SdkConfigBuilder().with_allow_handoff(False).build()
+
+ assert result.allow_handoff is False
+
def test_should_serialize_to_json_without_error(self):
result = (
SdkConfigBuilder()
@@ -55,6 +71,7 @@ def test_should_serialize_to_json_without_error(self):
.with_preset_issuing_country(self.SOME_PRESET_ISSUING_COUNTRY)
.with_success_url(self.SOME_SUCCESS_URL)
.with_error_url(self.SOME_ERROR_URL)
+ .with_privacy_policy_url(self.SOME_PRIVACY_POLICY_URL)
.build()
)
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 41f0a5e5..034c4b05 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
@@ -21,6 +21,7 @@ class SessionSpecTest(unittest.TestCase):
SOME_CLIENT_SESSION_TOKEN_TTL = 300
SOME_RESOURCES_TTL = 100000
SOME_USER_TRACKING_ID = "someUserTrackingId"
+ SOME_SESSION_DEADLINE = "2021-09-03T11:40:54.727619+02:00"
def test_should_build_correctly(self):
sdk_config_mock = Mock(spec=SdkConfig)
@@ -31,6 +32,7 @@ def test_should_build_correctly(self):
result = (
SessionSpecBuilder()
.with_client_session_token_ttl(self.SOME_CLIENT_SESSION_TOKEN_TTL)
+ .with_session_deadline(self.SOME_SESSION_DEADLINE)
.with_resources_ttl(self.SOME_RESOURCES_TTL)
.with_user_tracking_id(self.SOME_USER_TRACKING_ID)
.with_notifications(notification_mock)
@@ -49,6 +51,7 @@ def test_should_build_correctly(self):
assert requested_check_mock in result.requested_checks
assert len(result.requested_tasks) == 1
assert requested_task_mock in result.requested_tasks
+ assert result.session_deadline == self.SOME_SESSION_DEADLINE
def test_should_serialize_to_json_without_error(self):
sdk_config_mock = Mock(spec=SdkConfig)
@@ -66,6 +69,7 @@ def test_should_serialize_to_json_without_error(self):
result = (
SessionSpecBuilder()
.with_client_session_token_ttl(self.SOME_CLIENT_SESSION_TOKEN_TTL)
+ .with_session_deadline(self.SOME_SESSION_DEADLINE)
.with_resources_ttl(self.SOME_RESOURCES_TTL)
.with_user_tracking_id(self.SOME_USER_TRACKING_ID)
.with_notifications(notification_mock)
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 5a08c5a0..6607d2d9 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
@@ -49,6 +49,19 @@ def test_should_filter_zoom_liveness_resources(self):
assert len(result.liveness_capture) == 2
assert len(result.zoom_liveness_resources) == 1
+ def test_should_filter_static_liveness_resources(self):
+ data = {
+ "liveness_capture": [
+ {"liveness_type": "STATIC"},
+ {"liveness_type": "someUnknown"},
+ ]
+ }
+
+ result = ResourceContainer(data)
+
+ assert len(result.liveness_capture) == 2
+ assert len(result.static_liveness_resources) == 1
+
if __name__ == "__main__":
unittest.main()
diff --git a/yoti_python_sdk/tests/doc_scan/session/retrieve/test_static_liveness_resource.py b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_static_liveness_resource.py
new file mode 100644
index 00000000..0037810c
--- /dev/null
+++ b/yoti_python_sdk/tests/doc_scan/session/retrieve/test_static_liveness_resource.py
@@ -0,0 +1,72 @@
+import unittest
+from yoti_python_sdk.doc_scan.session.retrieve.static_liveness_resource_response import (
+ StaticLivenessResourceResponse,
+)
+from yoti_python_sdk.doc_scan.session.retrieve.image_response import ImageResponse
+from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse
+
+
+class StaticLivenessResourceResponseTest(unittest.TestCase):
+ def test_should_parse_static_liveness_resource(self):
+ data = {
+ "id": "bbbbbbb-5717-4562-b3fc-2c963f66afa6",
+ "source": {"type": "END_USER"},
+ "liveness_type": "STATIC",
+ "image": {
+ "media": {
+ "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
+ "type": "IMAGE",
+ "created": "2021-06-11T11:39:24Z",
+ "last_updated": "2021-06-11T11:39:24Z",
+ }
+ },
+ "tasks": [],
+ }
+
+ result = StaticLivenessResourceResponse(data)
+
+ assert result.id == "bbbbbbb-5717-4562-b3fc-2c963f66afa6"
+ assert result.liveness_type == "STATIC"
+ assert isinstance(result.image, ImageResponse)
+ assert isinstance(result.image.media, MediaResponse)
+ assert result.image.media.id == "3fa85f64-5717-4562-b3fc-2c963f66afa6"
+ assert result.image.media.type == "IMAGE"
+
+ def test_should_handle_missing_image(self):
+ data = {
+ "id": "test-id",
+ "liveness_type": "STATIC",
+ "tasks": [],
+ }
+
+ result = StaticLivenessResourceResponse(data)
+
+ assert result.id == "test-id"
+ assert result.liveness_type == "STATIC"
+ assert result.image is None
+
+ def test_should_parse_media_id_for_retrieval(self):
+ data = {
+ "id": "resource-id",
+ "liveness_type": "STATIC",
+ "image": {
+ "media": {
+ "id": "media-id-123",
+ "type": "IMAGE",
+ "created": "2021-06-11T11:39:24Z",
+ "last_updated": "2021-06-11T11:39:24Z",
+ }
+ },
+ "tasks": [],
+ }
+
+ result = StaticLivenessResourceResponse(data)
+
+ # Verify we can access the media ID for content retrieval
+ assert result.image is not None
+ assert result.image.media is not None
+ assert result.image.media.id == "media-id-123"
+
+
+if __name__ == "__main__":
+ unittest.main()
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
index 762d509d..a2887318 100644
--- a/yoti_python_sdk/tests/doc_scan/support/test_supported_documents.py
+++ b/yoti_python_sdk/tests/doc_scan/support/test_supported_documents.py
@@ -18,6 +18,24 @@ def test_supported_document_should_not_throw_exception_on_missing_data():
assert result.type is None
+def test_supported_document_created_with_is_strictly_latin_as_true():
+ result = SupportedDocument({"is_strictly_latin": True})
+
+ assert result.is_strictly_latin is True
+
+
+def test_supported_document_created_with_is_strictly_latin_as_false():
+ result = SupportedDocument({"is_strictly_latin": False})
+
+ assert result.is_strictly_latin is False
+
+
+def test_supported_document_created_without_is_strictly_latin():
+ result = SupportedDocument({"type": "someSupportedDocument"})
+
+ assert result.is_strictly_latin is None
+
+
def test_supported_country_should_parse_data():
data = {
"code": "someCode",
diff --git a/yoti_python_sdk/tests/dynamic_sharing_service/extension/test_third_party_attribute_extension.py b/yoti_python_sdk/tests/dynamic_sharing_service/extension/test_third_party_attribute_extension.py
index 7c7a57ae..1bf054ba 100644
--- a/yoti_python_sdk/tests/dynamic_sharing_service/extension/test_third_party_attribute_extension.py
+++ b/yoti_python_sdk/tests/dynamic_sharing_service/extension/test_third_party_attribute_extension.py
@@ -117,9 +117,18 @@ def test_should_format_utc_expiry_dates_correctly(expiry_date, expected_value):
@pytest.mark.parametrize(
"expiry_date, tz_name",
[
- (datetime(2030, 6, 6, 8, 0, 0, 0), "US/Eastern",),
- (datetime(2030, 6, 6, 15, 0, 0, 0), "Europe/Moscow",),
- (datetime(2030, 6, 6, 7, 0, 0, 0), "America/Jamaica",),
+ (
+ datetime(2030, 6, 6, 8, 0, 0, 0),
+ "US/Eastern",
+ ),
+ (
+ datetime(2030, 6, 6, 15, 0, 0, 0),
+ "Europe/Moscow",
+ ),
+ (
+ datetime(2030, 6, 6, 7, 0, 0, 0),
+ "America/Jamaica",
+ ),
(datetime(2030, 6, 6, 23, 0, 0, 0), "Etc/GMT-11"),
(datetime(2030, 6, 6, 7, 0, 0, 0), "Etc/GMT+5"),
# In order to conform with the POSIX style, those zones beginning
diff --git a/yoti_python_sdk/tests/test_anchor.py b/yoti_python_sdk/tests/test_anchor.py
index 0adf48aa..7305d6fa 100644
--- a/yoti_python_sdk/tests/test_anchor.py
+++ b/yoti_python_sdk/tests/test_anchor.py
@@ -1,13 +1,14 @@
# -*- coding: utf-8 -*-
import logging
+import pytz
import time
from datetime import datetime
-
-from yoti_python_sdk.protobuf.attribute_public_api import Attribute_pb2
+from datetime import timedelta
import yoti_python_sdk
from yoti_python_sdk import config
from yoti_python_sdk.anchor import Anchor
+from yoti_python_sdk.protobuf.attribute_public_api import Attribute_pb2
from yoti_python_sdk.tests import anchor_fixture_parser
@@ -118,11 +119,10 @@ def test_processing_unknown_anchor_data():
(anchor.value, anchor.anchor_type, anchor.sub_type) for anchor in anchors
]
- expected_timestamp = datetime(2019, 3, 5, 10, 45, 11, 840037)
- actual_timestamp = anchors[0].signed_timestamp
+ expected_timestamp = datetime(2019, 3, 5, 10, 45, 11, 840037, tzinfo=pytz.utc)
+ actual_timestamp = anchors[0].signed_timestamp.astimezone(pytz.utc)
assert expected_timestamp == actual_timestamp
-
assert "document-registration-server" in [
a.value for a in anchors[0].origin_server_certs.issuer
]
diff --git a/yoti_python_sdk/version.py b/yoti_python_sdk/version.py
index e8cf2158..821f13ed 100644
--- a/yoti_python_sdk/version.py
+++ b/yoti_python_sdk/version.py
@@ -1,2 +1,2 @@
# -*- coding: utf-8 -*-
-__version__ = "2.14.1"
+__version__ = "2.14.3"
\ No newline at end of file