Skip to content

Commit d1d8e58

Browse files
authored
added python static liveness check (#425)
* added python static liveness check
1 parent 0441863 commit d1d8e58

File tree

9 files changed

+869
-545
lines changed

9 files changed

+869
-545
lines changed

examples/doc_scan/templates/success.html

Lines changed: 595 additions & 541 deletions
Large diffs are not rendered by default.

yoti_python_sdk/doc_scan/constants.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
ID_DOCUMENT_FACE_MATCH = "ID_DOCUMENT_FACE_MATCH"
99
LIVENESS = "LIVENESS"
1010
ZOOM = "ZOOM"
11+
STATIC = "STATIC"
1112
SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK = "SUPPLEMENTARY_DOCUMENT_TEXT_DATA_CHECK"
1213
SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION = (
1314
"SUPPLEMENTARY_DOCUMENT_TEXT_DATA_EXTRACTION"

yoti_python_sdk/doc_scan/session/create/check/liveness.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,18 @@ class RequestedLivenessCheckConfig(YotiSerializable):
1111
The configuration applied when creating a Liveness Check
1212
"""
1313

14-
def __init__(self, liveness_type, max_retries):
14+
def __init__(self, liveness_type, max_retries, manual_check=None):
1515
"""
1616
:param liveness_type: the liveness type
1717
:type liveness_type: str
1818
:param max_retries: the maximum number of retries
1919
:type max_retries: int
20+
:param manual_check: the manual check value
21+
:type manual_check: str or None
2022
"""
2123
self.__liveness_type = liveness_type
2224
self.__max_retries = max_retries
25+
self.__manual_check = manual_check
2326

2427
@property
2528
def liveness_type(self):
@@ -39,9 +42,23 @@ def max_retries(self):
3942
"""
4043
return self.__max_retries
4144

45+
@property
46+
def manual_check(self):
47+
"""
48+
The manual check value for the liveness check
49+
50+
:return: the manual check value
51+
:rtype: str or None
52+
"""
53+
return self.__manual_check
54+
4255
def to_json(self):
4356
return remove_null_values(
44-
{"liveness_type": self.liveness_type, "max_retries": self.max_retries}
57+
{
58+
"liveness_type": self.liveness_type,
59+
"max_retries": self.max_retries,
60+
"manual_check": self.manual_check,
61+
}
4562
)
4663

4764

@@ -74,6 +91,7 @@ class RequestedLivenessCheckBuilder(object):
7491
def __init__(self):
7592
self.__liveness_type = None
7693
self.__max_retries = None
94+
self.__manual_check = None
7795

7896
def for_zoom_liveness(self):
7997
"""
@@ -84,6 +102,15 @@ def for_zoom_liveness(self):
84102
"""
85103
return self.with_liveness_type(constants.ZOOM)
86104

105+
def for_static_liveness(self):
106+
"""
107+
Sets the liveness type to "STATIC"
108+
109+
:return: the builder
110+
:rtype: RequestedLivenessCheckBuilder
111+
"""
112+
return self.with_liveness_type(constants.STATIC)
113+
87114
def with_liveness_type(self, liveness_type):
88115
"""
89116
Sets the liveness type on the builder
@@ -109,6 +136,18 @@ def with_max_retries(self, max_retries):
109136
self.__max_retries = max_retries
110137
return self
111138

139+
def with_manual_check_never(self):
140+
"""
141+
Sets the manual check value to "NEVER"
142+
143+
:return: the builder
144+
:rtype: RequestedLivenessCheckBuilder
145+
"""
146+
self.__manual_check = constants.NEVER
147+
return self
148+
112149
def build(self):
113-
config = RequestedLivenessCheckConfig(self.__liveness_type, self.__max_retries)
150+
config = RequestedLivenessCheckConfig(
151+
self.__liveness_type, self.__max_retries, self.__manual_check
152+
)
114153
return RequestedLivenessCheck(config)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from .media_response import MediaResponse
5+
6+
7+
class ImageResponse(object):
8+
"""
9+
Represents an image resource within a static liveness check
10+
"""
11+
12+
def __init__(self, data=None):
13+
"""
14+
:param data: the data to parse
15+
:type data: dict or None
16+
"""
17+
if data is None:
18+
data = dict()
19+
20+
self.__media = (
21+
MediaResponse(data["media"]) if "media" in data.keys() else None
22+
)
23+
24+
@property
25+
def media(self):
26+
"""
27+
Returns the media information for the image
28+
29+
:return: the media
30+
:rtype: MediaResponse or None
31+
"""
32+
return self.__media

yoti_python_sdk/doc_scan/session/retrieve/resource_container.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111
LivenessResourceResponse,
1212
ZoomLivenessResourceResponse,
1313
)
14+
from yoti_python_sdk.doc_scan.session.retrieve.static_liveness_resource_response import (
15+
StaticLivenessResourceResponse,
16+
)
1417

1518

1619
class ResourceContainer(object):
@@ -54,7 +57,7 @@ def __parse_liveness_capture(liveness_capture):
5457
:return: the parsed liveness capture
5558
:rtype: LivenessResourceResponse
5659
"""
57-
types = {"ZOOM": ZoomLivenessResourceResponse}
60+
types = {"ZOOM": ZoomLivenessResourceResponse, "STATIC": StaticLivenessResourceResponse}
5861

5962
clazz = types.get(
6063
liveness_capture.get("liveness_type", None),
@@ -105,3 +108,17 @@ def zoom_liveness_resources(self):
105108
for liveness in self.__liveness_capture
106109
if isinstance(liveness, ZoomLivenessResourceResponse)
107110
]
111+
112+
@property
113+
def static_liveness_resources(self):
114+
"""
115+
Returns a filtered list of static liveness capture resources
116+
117+
:return: list of static liveness captures
118+
:rtype: list[StaticLivenessResourceResponse]
119+
"""
120+
return [
121+
liveness
122+
for liveness in self.__liveness_capture
123+
if isinstance(liveness, StaticLivenessResourceResponse)
124+
]
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
from .liveness_resource_response import LivenessResourceResponse
5+
from .image_response import ImageResponse
6+
7+
8+
class StaticLivenessResourceResponse(LivenessResourceResponse):
9+
"""
10+
Represents a Static Liveness resource for a given session
11+
"""
12+
13+
def __init__(self, data=None):
14+
"""
15+
:param data: the data to parse
16+
:type data: dict or None
17+
"""
18+
if data is None:
19+
data = dict()
20+
21+
LivenessResourceResponse.__init__(self, data)
22+
23+
self.__image = (
24+
ImageResponse(data["image"]) if "image" in data.keys() else None
25+
)
26+
27+
@property
28+
def image(self):
29+
"""
30+
Returns the associated image for the static liveness resource
31+
32+
:return: the image
33+
:rtype: ImageResponse or None
34+
"""
35+
return self.__image

yoti_python_sdk/tests/doc_scan/session/create/check/test_liveness_check.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,67 @@ def test_should_serialize_to_json_without_error(self):
5252
s = json.dumps(result, cls=YotiEncoder)
5353
assert s is not None and s != ""
5454

55+
def test_should_build_with_static_liveness_type(self):
56+
result = (
57+
RequestedLivenessCheckBuilder()
58+
.for_static_liveness()
59+
.with_max_retries(3)
60+
.build()
61+
)
62+
63+
assert result.type == "LIVENESS"
64+
assert result.config.liveness_type == "STATIC"
65+
assert result.config.max_retries == 3
66+
67+
def test_should_build_with_manual_check_never(self):
68+
result = (
69+
RequestedLivenessCheckBuilder()
70+
.for_static_liveness()
71+
.with_max_retries(3)
72+
.with_manual_check_never()
73+
.build()
74+
)
75+
76+
assert result.config.liveness_type == "STATIC"
77+
assert result.config.manual_check == "NEVER"
78+
79+
def test_should_serialize_static_liveness_to_json(self):
80+
result = (
81+
RequestedLivenessCheckBuilder()
82+
.for_static_liveness()
83+
.with_max_retries(3)
84+
.with_manual_check_never()
85+
.build()
86+
)
87+
88+
json_str = json.dumps(result, cls=YotiEncoder)
89+
assert json_str is not None
90+
91+
# Verify the JSON contains the expected fields
92+
json_data = json.loads(json_str)
93+
assert json_data["type"] == "LIVENESS"
94+
assert json_data["config"]["liveness_type"] == "STATIC"
95+
assert json_data["config"]["manual_check"] == "NEVER"
96+
assert json_data["config"]["max_retries"] == 3
97+
98+
def test_should_omit_manual_check_when_not_set(self):
99+
result = (
100+
RequestedLivenessCheckBuilder()
101+
.for_static_liveness()
102+
.with_max_retries(3)
103+
.build()
104+
)
105+
106+
json_str = json.dumps(result, cls=YotiEncoder)
107+
assert json_str is not None
108+
109+
# Verify the JSON does not contain the manual_check field
110+
json_data = json.loads(json_str)
111+
assert json_data["type"] == "LIVENESS"
112+
assert json_data["config"]["liveness_type"] == "STATIC"
113+
assert "manual_check" not in json_data["config"]
114+
assert json_data["config"]["max_retries"] == 3
115+
55116

56117
if __name__ == "__main__":
57118
unittest.main()

yoti_python_sdk/tests/doc_scan/session/retrieve/test_resource_container.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,19 @@ def test_should_filter_zoom_liveness_resources(self):
4949
assert len(result.liveness_capture) == 2
5050
assert len(result.zoom_liveness_resources) == 1
5151

52+
def test_should_filter_static_liveness_resources(self):
53+
data = {
54+
"liveness_capture": [
55+
{"liveness_type": "STATIC"},
56+
{"liveness_type": "someUnknown"},
57+
]
58+
}
59+
60+
result = ResourceContainer(data)
61+
62+
assert len(result.liveness_capture) == 2
63+
assert len(result.static_liveness_resources) == 1
64+
5265

5366
if __name__ == "__main__":
5467
unittest.main()
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import unittest
2+
from yoti_python_sdk.doc_scan.session.retrieve.static_liveness_resource_response import (
3+
StaticLivenessResourceResponse,
4+
)
5+
from yoti_python_sdk.doc_scan.session.retrieve.image_response import ImageResponse
6+
from yoti_python_sdk.doc_scan.session.retrieve.media_response import MediaResponse
7+
8+
9+
class StaticLivenessResourceResponseTest(unittest.TestCase):
10+
def test_should_parse_static_liveness_resource(self):
11+
data = {
12+
"id": "bbbbbbb-5717-4562-b3fc-2c963f66afa6",
13+
"source": {"type": "END_USER"},
14+
"liveness_type": "STATIC",
15+
"image": {
16+
"media": {
17+
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
18+
"type": "IMAGE",
19+
"created": "2021-06-11T11:39:24Z",
20+
"last_updated": "2021-06-11T11:39:24Z",
21+
}
22+
},
23+
"tasks": [],
24+
}
25+
26+
result = StaticLivenessResourceResponse(data)
27+
28+
assert result.id == "bbbbbbb-5717-4562-b3fc-2c963f66afa6"
29+
assert result.liveness_type == "STATIC"
30+
assert isinstance(result.image, ImageResponse)
31+
assert isinstance(result.image.media, MediaResponse)
32+
assert result.image.media.id == "3fa85f64-5717-4562-b3fc-2c963f66afa6"
33+
assert result.image.media.type == "IMAGE"
34+
35+
def test_should_handle_missing_image(self):
36+
data = {
37+
"id": "test-id",
38+
"liveness_type": "STATIC",
39+
"tasks": [],
40+
}
41+
42+
result = StaticLivenessResourceResponse(data)
43+
44+
assert result.id == "test-id"
45+
assert result.liveness_type == "STATIC"
46+
assert result.image is None
47+
48+
def test_should_parse_media_id_for_retrieval(self):
49+
data = {
50+
"id": "resource-id",
51+
"liveness_type": "STATIC",
52+
"image": {
53+
"media": {
54+
"id": "media-id-123",
55+
"type": "IMAGE",
56+
"created": "2021-06-11T11:39:24Z",
57+
"last_updated": "2021-06-11T11:39:24Z",
58+
}
59+
},
60+
"tasks": [],
61+
}
62+
63+
result = StaticLivenessResourceResponse(data)
64+
65+
# Verify we can access the media ID for content retrieval
66+
assert result.image is not None
67+
assert result.image.media is not None
68+
assert result.image.media.id == "media-id-123"
69+
70+
71+
if __name__ == "__main__":
72+
unittest.main()

0 commit comments

Comments
 (0)