Skip to content

Commit 5cd75b6

Browse files
authored
Merge pull request #182 from getyoti/SDK-1299-DocScanClient
SDK-1299: Add initial support for Doc Scan
2 parents d995f1b + a7259b8 commit 5cd75b6

File tree

86 files changed

+4525
-18
lines changed

Some content is hidden

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

86 files changed

+4525
-18
lines changed

.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

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: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ cryptography==2.8 # via -r requirements.in, pyopenssl
1212
deprecated==1.2.6 # via -r requirements.in
1313
future==0.18.2 # via -r requirements.in
1414
idna==2.7 # via requests
15+
iso8601==0.1.12 # via -r requirements.in
1516
itsdangerous==0.24 # via -r requirements.in
1617
pbr==1.10.0 # via -r requirements.in
1718
protobuf==3.11.3 # via -r requirements.in

setup.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
"future>=0.11.0",
3232
"asn1==2.2.0",
3333
"pyopenssl>=18.0.0",
34+
"iso8601==0.1.12",
3435
],
3536
extras_require={
3637
"examples": [

yoti_python_sdk/__init__.py

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
"YOTI_API_URL": "https://api.yoti.com",
1010
"YOTI_API_PORT": 443,
1111
"YOTI_API_VERSION": "v1",
12-
"YOTI_API_VERIFY_SSL": "true"
12+
"YOTI_API_VERIFY_SSL": "true",
1313
}
1414

1515
main_ns = {}
@@ -23,19 +23,29 @@
2323

2424
__version__ = main_ns["__version__"]
2525
YOTI_API_URL = environ.get("YOTI_API_URL", DEFAULTS["YOTI_API_URL"])
26+
27+
YOTI_PROFILE_ENDPOINT = "/api/v1"
28+
YOTI_DOC_SCAN_ENDPOINT = "/idverify/v1"
29+
2630
YOTI_API_PORT = environ.get("YOTI_API_PORT", DEFAULTS["YOTI_API_PORT"])
2731
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-
))
3132

32-
YOTI_API_VERIFY_SSL = environ.get("YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"])
33+
# Fully formatted API URLs
34+
YOTI_API_ENDPOINT = environ.get(
35+
"YOTI_API_ENDPOINT",
36+
"{0}:{1}{2}".format(YOTI_API_URL, YOTI_API_PORT, YOTI_PROFILE_ENDPOINT),
37+
)
38+
YOTI_DOC_SCAN_API_URL = environ.get(
39+
"YOTI_DOC_SCAN_API_URL",
40+
"{0}:{1}{2}".format(YOTI_API_URL, YOTI_API_PORT, YOTI_DOC_SCAN_ENDPOINT),
41+
)
42+
43+
YOTI_API_VERIFY_SSL = environ.get(
44+
"YOTI_API_VERIFY_SSL", DEFAULTS["YOTI_API_VERIFY_SSL"]
45+
)
3346
if YOTI_API_VERIFY_SSL.lower() == "false":
3447
YOTI_API_VERIFY_SSL = False
3548
else:
3649
YOTI_API_VERIFY_SSL = True
3750

38-
__all__ = [
39-
"Client",
40-
__version__
41-
]
51+
__all__ = ["Client", __version__]
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/doc_scan/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.doc_scan.endpoint import Endpoint
8+
from yoti_python_sdk.doc_scan.session.retrieve.create_session_result import (
9+
CreateSessionResult,
10+
)
11+
from yoti_python_sdk.doc_scan.session.retrieve.get_session_result import (
12+
GetSessionResult,
13+
)
14+
from yoti_python_sdk.doc_scan.session.retrieve.media_value import MediaValue
15+
from yoti_python_sdk.http import SignedRequest
16+
from yoti_python_sdk.utils import YotiEncoder
17+
from .exception import DocScanException
18+
19+
20+
class DocScanClient(object):
21+
"""
22+
Client used for communication with the Yoti Doc Scan service where any
23+
signed request is required
24+
"""
25+
26+
def __init__(self, sdk_id, key, api_url=None):
27+
self.__sdk_id = sdk_id
28+
self.__key = key
29+
if api_url is not None:
30+
self.__api_url = api_url
31+
else:
32+
self.__api_url = yoti_python_sdk.YOTI_DOC_SCAN_ENDPOINT
33+
34+
def create_session(self, session_spec):
35+
"""
36+
Creates a Doc Scan session using the supplied session specification
37+
38+
:param session_spec: the session specification
39+
:type session_spec: SessionSpec
40+
:return: the create session result
41+
:rtype: CreateSessionResult
42+
:raises DocScanException: if there was an error creating the session
43+
"""
44+
payload = json.dumps(session_spec, cls=YotiEncoder).encode("utf-8")
45+
46+
request = (
47+
SignedRequest.builder()
48+
.with_post()
49+
.with_pem_file(self.__key)
50+
.with_base_url(self.__api_url)
51+
.with_endpoint(Endpoint.create_docs_session_path())
52+
.with_param("sdkId", self.__sdk_id)
53+
.with_payload(payload)
54+
.with_header("Content-Type", "application/json")
55+
.build()
56+
)
57+
response = request.execute()
58+
59+
if response.status_code != 201:
60+
raise DocScanException("Failed to create session", response)
61+
62+
data = json.loads(response.text)
63+
return CreateSessionResult(data)
64+
65+
def get_session(self, session_id):
66+
"""
67+
Retrieves the state of a previously created Yoti Doc Scan session
68+
69+
:param session_id: the session ID
70+
:type session_id: str
71+
:return: the session state
72+
:rtype: GetSessionResult
73+
:raises DocScanException: if there was an error retrieving the session
74+
"""
75+
request = (
76+
SignedRequest.builder()
77+
.with_get()
78+
.with_pem_file(self.__key)
79+
.with_base_url(self.__api_url)
80+
.with_endpoint(Endpoint.retrieve_docs_session_path(session_id))
81+
.with_param("sdkId", self.__sdk_id)
82+
.build()
83+
)
84+
response = request.execute()
85+
86+
if response.status_code != 200:
87+
raise DocScanException("Failed to retrieve session", response)
88+
89+
data = json.loads(response.text)
90+
return GetSessionResult(data)
91+
92+
def delete_session(self, session_id):
93+
"""
94+
Deletes a previously created Yoti Doc Scan session and
95+
all of its related resources
96+
97+
:param session_id: the session id to delete
98+
:type session_id: str
99+
:rtype: None
100+
:raises DocScanException: if there was an error deleting the session
101+
"""
102+
request = (
103+
SignedRequest.builder()
104+
.with_http_method("DELETE")
105+
.with_pem_file(self.__key)
106+
.with_base_url(self.__api_url)
107+
.with_endpoint(Endpoint.delete_docs_session_path(session_id))
108+
.with_param("sdkId", self.__sdk_id)
109+
.build()
110+
)
111+
response = request.execute()
112+
113+
if response.status_code < 200 or response.status_code >= 300:
114+
raise DocScanException("Failed to delete session", response)
115+
116+
def get_media_content(self, session_id, media_id):
117+
"""
118+
Retrieves media related to a Yoti Doc Scan session
119+
based on the supplied media ID
120+
121+
:param session_id: the session ID
122+
:type session_id: str
123+
:param media_id: the media ID
124+
:type media_id: str
125+
:return: the media
126+
:rtype: MediaValue
127+
:raises DocScanException: if there was an error retrieving the media content
128+
"""
129+
request = (
130+
SignedRequest.builder()
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)
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# -*- coding: utf-8 -*-
2+
from __future__ import unicode_literals
3+
4+
ID_DOCUMENT_AUTHENTICITY = "ID_DOCUMENT_AUTHENTICITY"
5+
ID_DOCUMENT_TEXT_DATA_CHECK = "ID_DOCUMENT_TEXT_DATA_CHECK"
6+
ID_DOCUMENT_TEXT_DATA_EXTRACTION = "ID_DOCUMENT_TEXT_DATA_EXTRACTION"
7+
ID_DOCUMENT_FACE_MATCH = "ID_DOCUMENT_FACE_MATCH"
8+
LIVENESS = "LIVENESS"
9+
ZOOM = "ZOOM"
10+
11+
CAMERA = "CAMERA"
12+
CAMERA_AND_UPLOAD = "CAMERA_AND_UPLOAD"
13+
14+
RESOURCE_UPDATE = "RESOURCE_UPDATE"
15+
TASK_COMPLETION = "TASK_COMPLETION"
16+
CHECK_COMPLETION = "CHECK_COMPLETION"
17+
SESSION_COMPLETION = "SESSION_COMPLETION"
18+
19+
ALWAYS = "ALWAYS"
20+
FALLBACK = "FALLBACK"
21+
NEVER = "NEVER"
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
class Endpoint(object):
2+
@staticmethod
3+
def create_docs_session_path():
4+
return "/sessions"
5+
6+
@staticmethod
7+
def retrieve_docs_session_path(session_id):
8+
return "/sessions/{sessionId}".format(sessionId=session_id)
9+
10+
@staticmethod
11+
def delete_docs_session_path(session_id):
12+
return Endpoint.retrieve_docs_session_path(session_id)
13+
14+
@staticmethod
15+
def get_media_content_path(session_id, media_id):
16+
return "/sessions/{sessionId}/media/{mediaId}/content".format(
17+
sessionId=session_id, mediaId=media_id
18+
)
19+
20+
@staticmethod
21+
def delete_media_path(session_id, media_id):
22+
return Endpoint.get_media_content_path(session_id, media_id)
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
from .doc_scan_exception import DocScanException
2+
3+
__all__ = [DocScanException]

0 commit comments

Comments
 (0)