Skip to content

Commit 806fdd6

Browse files
committed
SDK-1299: Add initial support for Doc Scan
1 parent d995f1b commit 806fdd6

81 files changed

Lines changed: 4402 additions & 32 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coveragerc

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,8 @@
22
omit =
33
yoti_python_sdk/tests/**
44
yoti_python_sdk/protobuf/**/*
5-
examples/**
5+
examples/**
6+
7+
[report]
8+
exclude_lines =
9+
raise NotImplementedError
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from yoti_python_sdk.docs import DocScanClient
2+
from yoti_python_sdk.docs import SessionSpecBuilder
3+
from yoti_python_sdk.docs.session.create.check import DocumentAuthenticityCheckBuilder
4+
from yoti_python_sdk.docs import NotificationConfigBuilder
5+
from yoti_python_sdk.docs import SDKConfigBuilder
6+
from os.path import join, dirname
7+
from flask import Flask, render_template
8+
from dotenv import load_dotenv
9+
10+
dotenv_path = join(dirname(__file__), ".env")
11+
load_dotenv(dotenv_path)
12+
from settings import YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH # noqa
13+
14+
app = Flask(__name__)
15+
16+
17+
@app.route("/")
18+
def index():
19+
client = DocScanClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH)
20+
session = client.create_session(
21+
SessionSpecBuilder()
22+
.with_requested_checks(DocumentAuthenticityCheckBuilder().build())
23+
.with_notifications(
24+
NotificationConfigBuilder()
25+
.with_endpoint("https://example.com")
26+
.with_topics("session_completion")
27+
.build()
28+
)
29+
.with_sdk_config(
30+
SDKConfigBuilder()
31+
.with_success_url("https://localhost:5000/success")
32+
.with_error_url("https://localhost:5000")
33+
.build()
34+
)
35+
.build()
36+
)
37+
print("Doc Scan Session ID: %s" % session.client_session_id)
38+
39+
return render_template(
40+
"index.html",
41+
session_id=session.client_session_id,
42+
client_token=session.client_session_token,
43+
)
44+
45+
46+
@app.route("/success")
47+
def success():
48+
return "Doc Scan request completed"
49+
50+
51+
@app.route("/session/<session_id>")
52+
def get_session(session_id):
53+
client = DocScanClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH)
54+
session = client.get_session(session_id)
55+
resources = [
56+
[page.media.id for page in document.pages]
57+
for document in session.resources.id_documents
58+
]
59+
return "Session status: %s, Page Resources: %s" % (session.state, resources)
60+
61+
62+
@app.route("/session/<session_id>/resource/<resource_id>")
63+
def get_resource(session_id, resource_id):
64+
client = DocScanClient(YOTI_CLIENT_SDK_ID, YOTI_KEY_FILE_PATH)
65+
resource = client.get_media_content(session_id, resource_id)
66+
67+
return '<img src="%s"/>' % resource.base64_content
68+
69+
70+
if __name__ == "__main__":
71+
app.run(host="0.0.0.0", ssl_context="adhoc")

examples/docscan_example_flask/requirements.txt

Whitespace-only changes.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from os import environ
2+
3+
YOTI_CLIENT_SDK_ID = environ.get("YOTI_CLIENT_SDK_ID")
4+
YOTI_KEY_FILE_PATH = environ.get("YOTI_KEY_FILE_PATH")
5+
YOTI_BASE_URL = environ.get("YOTI_BASE_URL")
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE html>
2+
<html class="yoti-html">
3+
4+
<head>
5+
<meta charset="utf-8" />
6+
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
7+
<title>Yoti Doc Scan example</title>
8+
<link rel="stylesheet" type="text/css" href="/static/index.css" />
9+
<link href="https://fonts.googleapis.com/css?family=Roboto:400,700" rel="stylesheet" />
10+
</head>
11+
12+
<body class="yoti-body">
13+
14+
<main>
15+
<h3>Doc Scan Session {{session_id}}</h3>
16+
<div style="width: 100%; text-align: center;">
17+
<iframe
18+
src="https://stg1.api.internal.yoti.com/idverify/v1/web/index.html?sessionID={{session_id}}&sessionToken={{client_token}}"
19+
width="100%" height="750" allow="camera"></iframe>
20+
</div>
21+
</main>
22+
23+
</body>
24+
25+
</html>
26+

requirements.in

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,4 @@ requests>=2.20.0
1212
urllib3>=1.24.2
1313
deprecated==1.2.6
1414
wheel==0.24.0
15+
iso8601==0.1.12

requirements.txt

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,26 +4,27 @@
44
#
55
# pip-compile --output-file=requirements.txt requirements.in
66
#
7-
asn1==2.2.0 # via -r requirements.in
7+
asn1==2.2.0
88
certifi==2018.11.29 # via requests
9-
cffi==1.13.0 # via -r requirements.in, cryptography
9+
cffi==1.13.0
1010
chardet==3.0.4 # via requests
11-
cryptography==2.8 # via -r requirements.in, pyopenssl
12-
deprecated==1.2.6 # via -r requirements.in
13-
future==0.18.2 # via -r requirements.in
11+
cryptography==2.8
12+
deprecated==1.2.6
13+
future==0.18.2
1414
idna==2.7 # via requests
15-
itsdangerous==0.24 # via -r requirements.in
16-
pbr==1.10.0 # via -r requirements.in
17-
protobuf==3.11.3 # via -r requirements.in
15+
iso8601==0.1.12
16+
itsdangerous==0.24
17+
pbr==1.10.0
18+
protobuf==3.11.3
1819
pycparser==2.18 # via cffi
19-
pyopenssl==18.0.0 # via -r requirements.in
20-
pytz==2018.9 # via -r requirements.in
21-
pyyaml==5.2 # via -r requirements.in
22-
requests==2.21.0 # via -r requirements.in
20+
pyopenssl==18.0.0
21+
pytz==2018.9
22+
pyyaml==5.2
23+
requests==2.21.0
2324
six==1.10.0 # via cryptography, protobuf, pyopenssl
24-
urllib3==1.24.2 # via -r requirements.in, requests
25-
wheel==0.24.0 # via -r requirements.in
25+
urllib3==1.24.2
26+
wheel==0.24.0
2627
wrapt==1.11.2 # via deprecated
2728

2829
# The following packages are considered to be unsafe in a requirements file:
29-
# setuptools
30+
# setuptools==46.1.3 # via protobuf

yoti_python_sdk/__init__.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,10 @@
77

88
DEFAULTS = {
99
"YOTI_API_URL": "https://api.yoti.com",
10+
"DOCS_API_URL": "https://stg1.api.internal.yoti.com/idverify/v1",
1011
"YOTI_API_PORT": 443,
1112
"YOTI_API_VERSION": "v1",
12-
"YOTI_API_VERIFY_SSL": "true"
13+
"YOTI_API_VERIFY_SSL": "true",
1314
}
1415

1516
main_ns = {}
@@ -23,19 +24,20 @@
2324

2425
__version__ = main_ns["__version__"]
2526
YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"])
27+
DOCS_API_URL = environ.get("DOCS_API_URL", DEFAULTS["DOCS_API_URL"])
2628
YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"])
2729
YOTI_API_VERSION = environ.get("YOTI_API_VERSION", DEFAULTS["YOTI_API_VERSION"])
28-
YOTI_API_ENDPOINT = environ.get("YOTI_API_ENDPOINT", "{0}:{1}/api/{2}".format(
29-
YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION
30-
))
31-
32-
YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"])
30+
YOTI_API_ENDPOINT = environ.get(
31+
"YOTI_API_ENDPOINT",
32+
"{0}:{1}/api/{2}".format(YOTI_API_URL, YOTI_API_PORT, YOTI_API_VERSION),
33+
)
34+
35+
YOTI_API_VERIFY_SSL = environ.get(
36+
"YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]
37+
)
3338
if YOTI_API_VERIFY_SSL.lower() == "false":
3439
YOTI_API_VERIFY_SSL = False
3540
else:
3641
YOTI_API_VERIFY_SSL = True
3742

38-
__all__ = [
39-
"Client",
40-
__version__
41-
]
43+
__all__ = ["Client", __version__]

yoti_python_sdk/docs/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
from .session.create.check.document_authenticity import (
2+
RequestedDocumentAuthenticityCheckBuilder,
3+
)
4+
from .session.create.check.face_match import RequestedFaceMatchCheckBuilder
5+
from .session.create.check.liveness import RequestedLivenessCheckBuilder
6+
from .session.create.task.text_extraction import RequestedTextExtractionTaskBuilder
7+
from .session.create.notification_config import NotificationConfigBuilder
8+
from .session.create.sdk_config import SdkConfigBuilder
9+
from .session.create.session_spec import SessionSpecBuilder
10+
11+
__all__ = [
12+
RequestedDocumentAuthenticityCheckBuilder,
13+
RequestedLivenessCheckBuilder,
14+
RequestedFaceMatchCheckBuilder,
15+
RequestedTextExtractionTaskBuilder,
16+
SessionSpecBuilder,
17+
NotificationConfigBuilder,
18+
SdkConfigBuilder,
19+
]

yoti_python_sdk/docs/client.py

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
import json
5+
6+
import yoti_python_sdk
7+
from yoti_python_sdk.docs.endpoint import Endpoint
8+
from yoti_python_sdk.docs.session.retrieve.create_session_result import (
9+
CreateSessionResult,
10+
)
11+
from yoti_python_sdk.docs.session.retrieve.get_session_result import GetSessionResult
12+
from yoti_python_sdk.docs.session.retrieve.media_value import MediaValue
13+
from yoti_python_sdk.http import MediaRequestHandler
14+
from yoti_python_sdk.http import SignedRequest
15+
from yoti_python_sdk.utils import YotiEncoder
16+
from .exception import DocScanException
17+
18+
19+
class DocScanClient(object):
20+
"""
21+
Client used for communication with the Yoti Doc Scan service where any
22+
signed request is required
23+
"""
24+
25+
def __init__(self, sdk_id, key, api_url=None):
26+
self.__sdk_id = sdk_id
27+
self.__key = key
28+
if api_url is not None:
29+
self.__api_url = api_url
30+
else:
31+
self.__api_url = yoti_python_sdk.DOCS_API_URL
32+
33+
def create_session(self, session_spec):
34+
"""
35+
Creates a Doc Scan session using the supplied session specification
36+
37+
:param session_spec: the session specification
38+
:type session_spec: SessionSpec
39+
:return: the create session result
40+
:rtype: CreateSessionResult
41+
:raises DocScanException: if there was an error creating the session
42+
"""
43+
payload = json.dumps(session_spec, cls=YotiEncoder).encode("utf-8")
44+
45+
request = (
46+
SignedRequest.builder()
47+
.with_post()
48+
.with_pem_file(self.__key)
49+
.with_base_url(self.__api_url)
50+
.with_endpoint(Endpoint.create_docs_session_path())
51+
.with_param("sdkId", self.__sdk_id)
52+
.with_payload(payload)
53+
.with_header("Content-Type", "application/json")
54+
.build()
55+
)
56+
response = request.execute()
57+
58+
if response.status_code != 201:
59+
raise DocScanException("Failed to create session", response)
60+
61+
data = json.loads(response.text)
62+
return CreateSessionResult(data)
63+
64+
def get_session(self, session_id):
65+
"""
66+
Retrieves the state of a previously created Yoti Doc Scan session
67+
68+
:param session_id: the session ID
69+
:type session_id: str
70+
:return: the session state
71+
:rtype: GetSessionResult
72+
:raises DocScanException: if there was an error retrieving the session
73+
"""
74+
request = (
75+
SignedRequest.builder()
76+
.with_get()
77+
.with_pem_file(self.__key)
78+
.with_base_url(self.__api_url)
79+
.with_endpoint(Endpoint.retrieve_docs_session_path(session_id))
80+
.with_param("sdkId", self.__sdk_id)
81+
.build()
82+
)
83+
response = request.execute()
84+
85+
if response.status_code != 200:
86+
raise DocScanException("Failed to retrieve session", response)
87+
88+
data = json.loads(response.text)
89+
return GetSessionResult(data)
90+
91+
def delete_session(self, session_id):
92+
"""
93+
Deletes a previously created Yoti Doc Scan session and
94+
all of its related resources
95+
96+
:param session_id: the session id to delete
97+
:type session_id: str
98+
:rtype: None
99+
:raises DocScanException: if there was an error deleting the session
100+
"""
101+
request = (
102+
SignedRequest.builder()
103+
.with_http_method("DELETE")
104+
.with_pem_file(self.__key)
105+
.with_base_url(self.__api_url)
106+
.with_endpoint(Endpoint.delete_docs_session_path(session_id))
107+
.with_param("sdkId", self.__sdk_id)
108+
.build()
109+
)
110+
response = request.execute()
111+
112+
if response.status_code < 200 or response.status_code >= 300:
113+
raise DocScanException("Failed to delete session", response)
114+
115+
def get_media_content(self, session_id, media_id):
116+
"""
117+
Retrieves media related to a Yoti Doc Scan session
118+
based on the supplied media ID
119+
120+
:param session_id: the session ID
121+
:type session_id: str
122+
:param media_id: the media ID
123+
:type media_id: str
124+
:return: the media
125+
:rtype: MediaValue
126+
:raises DocScanException: if there was an error retrieving the media content
127+
"""
128+
request = (
129+
SignedRequest.builder()
130+
.with_request_handler(MediaRequestHandler)
131+
.with_get()
132+
.with_pem_file(self.__key)
133+
.with_base_url(self.__api_url)
134+
.with_endpoint(Endpoint.get_media_content_path(session_id, media_id))
135+
.with_param("sdkId", self.__sdk_id)
136+
.build()
137+
)
138+
response = request.execute()
139+
140+
if response.status_code != 200:
141+
raise DocScanException("Failed to retrieve media content", response)
142+
143+
media_mime_type = response.headers["Content-Type"]
144+
media_content = response.content
145+
return MediaValue(media_mime_type, media_content)
146+
147+
def delete_media_content(self, session_id, media_id):
148+
"""
149+
Deletes media related to a Yoti Doc Scan session
150+
based on the supplied media ID
151+
152+
:param session_id: the session ID
153+
:type session_id: str
154+
:param media_id: the media ID
155+
:type media_id: str
156+
:rtype: None
157+
:raises DocScanException: if there was an error deleting the media content
158+
"""
159+
request = (
160+
SignedRequest.builder()
161+
.with_http_method("DELETE")
162+
.with_pem_file(self.__key)
163+
.with_base_url(self.__api_url)
164+
.with_endpoint(Endpoint.delete_media_path(session_id, media_id))
165+
.with_param("sdkId", self.__sdk_id)
166+
.build()
167+
)
168+
169+
response = request.execute()
170+
if response.status_code < 200 or response.status_code >= 300:
171+
raise DocScanException("Failed to delete media content", response)

0 commit comments

Comments
 (0)