From 05f15b0f0006bfb2d8e880f9ca8a368eeba5b7d2 Mon Sep 17 00:00:00 2001 From: Bulakh Serhii Date: Wed, 8 Jan 2020 17:36:07 +0200 Subject: [PATCH 1/6] Implement send input method --- mati/api_service.py | 21 +++++++++++++++++++-- mati/call_http.py | 13 ++++++++++--- mati/types.py | 13 +++++++++++++ setup.py | 1 + 4 files changed, 43 insertions(+), 5 deletions(-) diff --git a/mati/api_service.py b/mati/api_service.py index 280103b..c0b18d7 100644 --- a/mati/api_service.py +++ b/mati/api_service.py @@ -1,10 +1,12 @@ import hashlib import hmac +import json from base64 import b64encode -from typing import Optional +from requests_toolbelt import MultipartEncoder +from typing import Dict, List, Optional from mati.call_http import RequestOptions, call_http, ErrorResponse -from mati.types import AuthType, IdentityMetadata, IdentityResource +from mati.types import AuthType, IdentityMetadata, IdentityResource, InputData API_HOST = 'https://api.getmati.com' @@ -75,6 +77,21 @@ def create_identity(self, metadata: IdentityMetadata) -> IdentityResource: ) ) + def send_input(self, identity_id: str, input_data: InputData) -> List[Dict[str, bool]]: + files = [('inputs', json.dumps(input_data.inputs))] + for fileOptions in input_data.files: + files.append((fileOptions.fieldName, fileOptions.fileData)) + encoder = MultipartEncoder(files) + endpoint = 'v2/identities/{identity_id}/send-input' + return self._call_http( + path=endpoint.format(identity_id=identity_id), + request_options=RequestOptions( + method='post', + body=encoder, + headers={'Content-Type': encoder.content_type}, + ) + ) + def _set_client_auth(self, client_id, client_secret): auth = b64encode(f'{client_id}:{client_secret}'.encode('utf-8')) self.client_auth_header = 'Basic ' + auth.decode('ascii') diff --git a/mati/call_http.py b/mati/call_http.py index b5d7cef..5df4ca7 100644 --- a/mati/call_http.py +++ b/mati/call_http.py @@ -1,4 +1,5 @@ -from typing import Dict +from typing import Dict, Union +from requests_toolbelt import MultipartEncoder from attr import dataclass from requests import Session, Response @@ -10,7 +11,8 @@ class RequestOptions: method: str = 'get' headers: Dict[str, str] = None - body: Dict = None + body: Union[Dict, MultipartEncoder] = None + data: Dict = None class ErrorResponse(Exception): @@ -19,7 +21,12 @@ class ErrorResponse(Exception): def call_http(request_url: str, request_options: RequestOptions): - response = session.request(request_options.method, request_url, headers=request_options.headers) + response = session.request( + request_options.method, + request_url, + headers=request_options.headers, + data=request_options.data, + ) if not response.ok: print(f'response.text: {response.text}') raise ErrorResponse(response.text, response) diff --git a/mati/types.py b/mati/types.py index 7a4836a..4899f54 100644 --- a/mati/types.py +++ b/mati/types.py @@ -60,6 +60,19 @@ class UserValidationFile: IdentityMetadata = Union[dict, List[str]] + +@dataclass +class FileOptions: + fieldName: str + fileData: Union[BinaryIO, str] + + +@dataclass +class InputData: + files: List[FileOptions] + inputs: Dict[str, Union[str, int]] + + @dataclass class IdentityStatusTypes(SerializableEnum): deleted = 'deleted', diff --git a/setup.py b/setup.py index 1dce3f7..70c96c3 100644 --- a/setup.py +++ b/setup.py @@ -35,6 +35,7 @@ 'dataclasses>=0.6;python_version<"3.7"', 'requests>=2.22.0,<3.0.0', 'iso8601>=0.1.12,<0.2.0', + 'requests_toolbelt>=0.8.0,<1.0.0', ], setup_requires=['pytest-runner'], tests_require=test_requires, From 4a367705cdab314585602c7ede6b72c035b2d044 Mon Sep 17 00:00:00 2001 From: Bulakh Serhii Date: Thu, 9 Jan 2020 10:42:59 +0200 Subject: [PATCH 2/6] Implement send input method Fix types --- mati/call_http.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mati/call_http.py b/mati/call_http.py index 5df4ca7..3ec1542 100644 --- a/mati/call_http.py +++ b/mati/call_http.py @@ -12,7 +12,6 @@ class RequestOptions: method: str = 'get' headers: Dict[str, str] = None body: Union[Dict, MultipartEncoder] = None - data: Dict = None class ErrorResponse(Exception): @@ -25,7 +24,7 @@ def call_http(request_url: str, request_options: RequestOptions): request_options.method, request_url, headers=request_options.headers, - data=request_options.data, + data=request_options.body, ) if not response.ok: print(f'response.text: {response.text}') From 2537ced1e4ffd2ef6ed27141ce5f799651513de4 Mon Sep 17 00:00:00 2001 From: Bulakh Serhii Date: Thu, 9 Jan 2020 11:31:24 +0200 Subject: [PATCH 3/6] Implement send input method Extend types.py --- mati/api_service.py | 4 +- mati/resources/user_verification_data.py | 4 +- mati/resources/verifications.py | 4 +- mati/types.py | 66 ++++++++++++++++++------ 4 files changed, 57 insertions(+), 21 deletions(-) diff --git a/mati/api_service.py b/mati/api_service.py index c0b18d7..cfbdb8f 100644 --- a/mati/api_service.py +++ b/mati/api_service.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional from mati.call_http import RequestOptions, call_http, ErrorResponse -from mati.types import AuthType, IdentityMetadata, IdentityResource, InputData +from mati.types import AuthType, IdentityMetadata, IdentityResource, InputRequestData API_HOST = 'https://api.getmati.com' @@ -77,7 +77,7 @@ def create_identity(self, metadata: IdentityMetadata) -> IdentityResource: ) ) - def send_input(self, identity_id: str, input_data: InputData) -> List[Dict[str, bool]]: + def send_input(self, identity_id: str, input_data: InputRequestData) -> List[Dict[str, bool]]: files = [('inputs', json.dumps(input_data.inputs))] for fileOptions in input_data.files: files.append((fileOptions.fieldName, fileOptions.fileData)) diff --git a/mati/resources/user_verification_data.py b/mati/resources/user_verification_data.py index e7a0f7c..e33d993 100644 --- a/mati/resources/user_verification_data.py +++ b/mati/resources/user_verification_data.py @@ -1,7 +1,7 @@ import json from typing import Any, BinaryIO, ClassVar, Dict, List, Tuple -from mati.types import UserValidationFile, ValidationInputType +from mati.types import UserValidationFile, VerificationInputType from .base import Resource @@ -28,7 +28,7 @@ class UserValidationData(Resource): def _append_file( files_metadata: List[Dict[str, Any]], file: UserValidationFile ): - if file.input_type == ValidationInputType.document_photo: + if file.input_type == VerificationInputType.document_photo: files_metadata.append( dict( inputType=file.input_type, diff --git a/mati/resources/verifications.py b/mati/resources/verifications.py index 066a49f..f47899b 100644 --- a/mati/resources/verifications.py +++ b/mati/resources/verifications.py @@ -1,7 +1,7 @@ from dataclasses import dataclass, field from typing import Any, ClassVar, Dict, List, Optional -from ..types import VerificationDocument, VerificationDocumentStep +from ..types import VerificationDocument, VerificationStep from .base import Resource @@ -26,7 +26,7 @@ def retrieve(cls, verification_id: str) -> 'Verification': docs = [] for doc in resp['documents']: doc['steps'] = [ - VerificationDocumentStep(**step) for step in doc['steps'] + VerificationStep(**step) for step in doc['steps'] ] docs.append(VerificationDocument(**doc)) resp['documents'] = docs diff --git a/mati/types.py b/mati/types.py index 4899f54..bf4035a 100644 --- a/mati/types.py +++ b/mati/types.py @@ -13,13 +13,13 @@ class PageType(SerializableEnum): back = 'back' -class ValidationInputType(SerializableEnum): +class VerificationInputType(SerializableEnum): document_photo = 'document-photo' selfie_photo = 'selfie-photo' selfie_video = 'selfie-video' -class ValidationType(SerializableEnum): +class DocumentType(SerializableEnum): driving_license = 'driving-license' national_id = 'national-id' passport = 'passport' @@ -27,7 +27,7 @@ class ValidationType(SerializableEnum): @dataclass -class VerificationDocumentStep: +class VerificationStep: id: str status: int error: Optional[str] = None @@ -39,7 +39,7 @@ class VerificationDocument: country: str region: str photos: List[str] - steps: List[VerificationDocumentStep] + steps: List[VerificationStep] type: str fields: Optional[dict] = None @@ -48,8 +48,8 @@ class VerificationDocument: class UserValidationFile: filename: str content: BinaryIO - input_type: Union[str, ValidationInputType] - validation_type: Union[str, ValidationType] = '' + input_type: Union[str, VerificationInputType] + validation_type: Union[str, DocumentType] = '' country: str = '' # alpha-2 code: https://www.iban.com/country-codes region: str = '' # 2-digit US State code (if applicable) group: int = 0 @@ -69,24 +69,60 @@ class FileOptions: @dataclass class InputData: + filename: str + + +@dataclass +class PhotoInputData(InputData): + type: DocumentType + page: PageType + country: str + region: str = None + + +@dataclass +class SelfieVideoInputData(InputData): + pass + + +@dataclass +class SelfiePhotoInputData(InputData): + pass + + +class Input(object): + def __init__( + self, + input_type: VerificationInputType, + data: Union[PhotoInputData, SelfiePhotoInputData, SelfieVideoInputData], + group: int = None + ): + self.data = data + self.inputType = input_type + if group is not None: + self.group = group + + +@dataclass +class InputRequestData: files: List[FileOptions] - inputs: Dict[str, Union[str, int]] + inputs: List[Input] @dataclass class IdentityStatusTypes(SerializableEnum): - deleted = 'deleted', - pending = 'pending', - rejected = 'rejected', - review_needed = 'reviewNeeded', - running = 'running', - verified = 'verified', + deleted = 'deleted', + pending = 'pending', + rejected = 'rejected', + review_needed = 'reviewNeeded', + running = 'running', + verified = 'verified', @dataclass class IdentityResource: - id: str - status: IdentityStatusTypes + id: str + status: IdentityStatusTypes @dataclass From 04e79d88f60ff9d5361b58824aeee197a40ca765 Mon Sep 17 00:00:00 2001 From: Bulakh Serhii Date: Thu, 9 Jan 2020 14:22:53 +0200 Subject: [PATCH 4/6] Implement send input method Add tests Add cassette Add method description --- mati/api_service.py | 18 ++++- mati/types.py | 25 ++++-- .../test_api_service_send_input.yaml | 79 +++++++++++++++++++ .../resources/test_user_verification_data.py | 14 ++-- tests/test_api_service.py | 54 +++++++++++++ tests/test_types.py | 6 +- 6 files changed, 174 insertions(+), 22 deletions(-) create mode 100644 tests/cassettes/test_api_service_send_input.yaml diff --git a/mati/api_service.py b/mati/api_service.py index cfbdb8f..dafb9d4 100644 --- a/mati/api_service.py +++ b/mati/api_service.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional from mati.call_http import RequestOptions, call_http, ErrorResponse -from mati.types import AuthType, IdentityMetadata, IdentityResource, InputRequestData +from mati.types import AuthType, IdentityMetadata, IdentityResource, InputsData API_HOST = 'https://api.getmati.com' @@ -77,9 +77,19 @@ def create_identity(self, metadata: IdentityMetadata) -> IdentityResource: ) ) - def send_input(self, identity_id: str, input_data: InputRequestData) -> List[Dict[str, bool]]: - files = [('inputs', json.dumps(input_data.inputs))] - for fileOptions in input_data.files: + def send_input(self, identity_id: str, inputs_data: InputsData) -> List[Dict[str, bool]]: + """ + Sends inputs data to process identity verification. Inputs can contain combined + document/selfie photo/selfie video data input and should represent merchant's + "Verification requirements" and "Biometric requirements" configurations to complete verification. + + :param {identity_id} identity_id: an identity id obtained from #create_identity response + :param {InputsData} inputs_data: contains files data and metadata field and files itself + :return: ordered list with result of upload (order in the list corresponds to inputs order) + :raise ErrorResponse if we get http error + """ + files = [('inputs', json.dumps(inputs_data.inputs))] + for fileOptions in inputs_data.files: files.append((fileOptions.fieldName, fileOptions.fileData)) encoder = MultipartEncoder(files) endpoint = 'v2/identities/{identity_id}/send-input' diff --git a/mati/types.py b/mati/types.py index bf4035a..5f25c1a 100644 --- a/mati/types.py +++ b/mati/types.py @@ -1,4 +1,4 @@ -from dataclasses import dataclass, field +from dataclasses import asdict, dataclass, field from enum import Enum from typing import BinaryIO, Dict, List, Optional, Union @@ -62,7 +62,7 @@ class UserValidationFile: @dataclass -class FileOptions: +class MediaInputOptions: fieldName: str fileData: Union[BinaryIO, str] @@ -90,22 +90,31 @@ class SelfiePhotoInputData(InputData): pass -class Input(object): +class Input(dict): def __init__( self, input_type: VerificationInputType, data: Union[PhotoInputData, SelfiePhotoInputData, SelfieVideoInputData], group: int = None ): - self.data = data - self.inputType = input_type if group is not None: - self.group = group + dict.__init__( + self, + input_type=input_type, + data=asdict(data), + group=group, + ) + else: + dict.__init__( + self, + input_type=input_type, + data=asdict(data), + ) @dataclass -class InputRequestData: - files: List[FileOptions] +class InputsData: + files: List[MediaInputOptions] inputs: List[Input] diff --git a/tests/cassettes/test_api_service_send_input.yaml b/tests/cassettes/test_api_service_send_input.yaml new file mode 100644 index 0000000..9a2dede --- /dev/null +++ b/tests/cassettes/test_api_service_send_input.yaml @@ -0,0 +1,79 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.22.0 + content-type: + - application/x-www-form-urlencoded + method: POST + uri: https://api.getmati.com/oauth + response: + body: + string: '{"access_token": "ACCESS_TOKEN", "expiresIn": 3600, "payload": {"user": + {"_id": "ID"}}}' + headers: + Connection: + - keep-alive + Content-Length: + - '452' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 13:57:28 GMT + X-Request-Id: + - b4663380-a927-4564-ade7-df32249d9ac4 + status: + code: 200 + message: OK +- request: + body: + string: '' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate + Connection: + - keep-alive + User-Agent: + - python-requests/2.22.0 + method: POST + uri: https://api.getmati.com/v2/identities/identityId/send-input + response: + body: + string: '[{"result":true},{"result":true},{"result":true}]' + headers: + Access-Control-Allow-Origin: + - '*' + Connection: + - keep-alive + Content-Length: + - '23' + Content-Type: + - application/json; charset=utf-8 + Date: + - Thu, 26 Dec 2019 14:09:44 GMT + Etag: + - W/"8f-ejjPa5NRLKui634I0HWy1uDz4OE" + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + X-Frame-Options: + - SAMEORIGIN + X-Powered-By: + - Express + X-Xss-Protection: + - 1; mode=block + status: + code: 201 + message: OK + +version: 1 diff --git a/tests/resources/test_user_verification_data.py b/tests/resources/test_user_verification_data.py index daf11e1..2d6e862 100644 --- a/tests/resources/test_user_verification_data.py +++ b/tests/resources/test_user_verification_data.py @@ -7,8 +7,8 @@ from mati.types import ( PageType, UserValidationFile, - ValidationInputType, - ValidationType, + VerificationInputType, + DocumentType, ) FIXTURE_DIR = os.path.join( @@ -28,22 +28,22 @@ def test_ine_and_liveness_upload(identity: Identity): user_validation_file = UserValidationFile( filename='ine_front.jpg', content=front, - input_type=ValidationInputType.document_photo, - validation_type=ValidationType.national_id, + input_type=VerificationInputType.document_photo, + validation_type=DocumentType.national_id, country='MX', ) user_validation_file_back = UserValidationFile( filename='ine_back.jpg', content=back, - input_type=ValidationInputType.document_photo, - validation_type=ValidationType.national_id, + input_type=VerificationInputType.document_photo, + validation_type=DocumentType.national_id, country='MX', page=PageType.back, ) user_validation_live = UserValidationFile( filename='liveness.MOV', content=live, - input_type=ValidationInputType.selfie_video, + input_type=VerificationInputType.selfie_video, ) resp = identity.upload_validation_data( [ diff --git a/tests/test_api_service.py b/tests/test_api_service.py index c8f83fe..f359201 100644 --- a/tests/test_api_service.py +++ b/tests/test_api_service.py @@ -1,9 +1,20 @@ from contextlib import contextmanager import pytest +import io from mati import ApiService from mati.call_http import ErrorResponse +from mati.types import ( + DocumentType, + Input, + InputsData, + MediaInputOptions, + PageType, + PhotoInputData, + SelfieVideoInputData, + VerificationInputType, +) client_id: str = 'clientId' client_secret: str = 'clientSecret' @@ -84,3 +95,46 @@ def test_api_service_create_identity(): api_service = ApiService() api_service.init(client_id, client_secret) assert api_service.create_identity({'email': 'john@gmail.com'}) + + +@pytest.mark.vcr +def test_api_service_send_input(): + api_service = ApiService() + api_service.init(client_id, client_secret) + inputs = [ + Input( + input_type=VerificationInputType.document_photo, + group=0, + data=PhotoInputData( + type=DocumentType.national_id, + country='MX', + page=PageType.front, + filename='front.jpg' + ) + ), + Input( + input_type=VerificationInputType.document_photo, + group=0, + data=PhotoInputData( + type=DocumentType.national_id, + country='MX', + page=PageType.back, + filename='back.jpg' + ) + ), + Input( + input_type=VerificationInputType.selfie_video, + data=SelfieVideoInputData( + filename='selfie.mp4', + ) + ), + ] + files = [ + MediaInputOptions('document', io.BytesIO()), + MediaInputOptions('document', io.BytesIO()), + MediaInputOptions('video', io.BytesIO()), + ] + assert api_service.send_input( + identity_id='identityId', + inputs_data=InputsData(files, inputs), + ) diff --git a/tests/test_types.py b/tests/test_types.py index 2c5679d..ada8f98 100644 --- a/tests/test_types.py +++ b/tests/test_types.py @@ -1,6 +1,6 @@ -from mati.types import ValidationInputType +from mati.types import VerificationInputType def test_type_to_str(): - assert str(ValidationInputType.document_photo) == 'document-photo' - assert ValidationInputType.document_photo == 'document-photo' + assert str(VerificationInputType.document_photo) == 'document-photo' + assert VerificationInputType.document_photo == 'document-photo' From 3e423951a0f27e33e7559735b186f82997460e7e Mon Sep 17 00:00:00 2001 From: Bulakh Serhii Date: Thu, 9 Jan 2020 15:04:31 +0200 Subject: [PATCH 5/6] Implement send input method Rename InputsData class to SendInputRequest --- mati/api_service.py | 10 +++++----- mati/types.py | 2 +- tests/test_api_service.py | 4 ++-- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/mati/api_service.py b/mati/api_service.py index dafb9d4..a3de11b 100644 --- a/mati/api_service.py +++ b/mati/api_service.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional from mati.call_http import RequestOptions, call_http, ErrorResponse -from mati.types import AuthType, IdentityMetadata, IdentityResource, InputsData +from mati.types import AuthType, IdentityMetadata, IdentityResource, SendInputRequest API_HOST = 'https://api.getmati.com' @@ -77,19 +77,19 @@ def create_identity(self, metadata: IdentityMetadata) -> IdentityResource: ) ) - def send_input(self, identity_id: str, inputs_data: InputsData) -> List[Dict[str, bool]]: + def send_input(self, identity_id: str, send_input_request: SendInputRequest) -> List[Dict[str, bool]]: """ Sends inputs data to process identity verification. Inputs can contain combined document/selfie photo/selfie video data input and should represent merchant's "Verification requirements" and "Biometric requirements" configurations to complete verification. :param {identity_id} identity_id: an identity id obtained from #create_identity response - :param {InputsData} inputs_data: contains files data and metadata field and files itself + :param {SendInputRequest} send_input_request: contains files data and metadata field and files itself :return: ordered list with result of upload (order in the list corresponds to inputs order) :raise ErrorResponse if we get http error """ - files = [('inputs', json.dumps(inputs_data.inputs))] - for fileOptions in inputs_data.files: + files = [('inputs', json.dumps(send_input_request.inputs))] + for fileOptions in send_input_request.files: files.append((fileOptions.fieldName, fileOptions.fileData)) encoder = MultipartEncoder(files) endpoint = 'v2/identities/{identity_id}/send-input' diff --git a/mati/types.py b/mati/types.py index 5f25c1a..e7c5ee3 100644 --- a/mati/types.py +++ b/mati/types.py @@ -113,7 +113,7 @@ def __init__( @dataclass -class InputsData: +class SendInputRequest: files: List[MediaInputOptions] inputs: List[Input] diff --git a/tests/test_api_service.py b/tests/test_api_service.py index f359201..2f9ea42 100644 --- a/tests/test_api_service.py +++ b/tests/test_api_service.py @@ -8,7 +8,7 @@ from mati.types import ( DocumentType, Input, - InputsData, + SendInputRequest, MediaInputOptions, PageType, PhotoInputData, @@ -136,5 +136,5 @@ def test_api_service_send_input(): ] assert api_service.send_input( identity_id='identityId', - inputs_data=InputsData(files, inputs), + send_input_request=SendInputRequest(files, inputs), ) From 3da0b7cab4789e64d6502818435824c780526330 Mon Sep 17 00:00:00 2001 From: Bulakh Serhii Date: Thu, 9 Jan 2020 18:09:46 +0200 Subject: [PATCH 6/6] Implement send input method Use f-string --- mati/api_service.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/mati/api_service.py b/mati/api_service.py index a3de11b..a1ed075 100644 --- a/mati/api_service.py +++ b/mati/api_service.py @@ -92,9 +92,8 @@ def send_input(self, identity_id: str, send_input_request: SendInputRequest) -> for fileOptions in send_input_request.files: files.append((fileOptions.fieldName, fileOptions.fileData)) encoder = MultipartEncoder(files) - endpoint = 'v2/identities/{identity_id}/send-input' return self._call_http( - path=endpoint.format(identity_id=identity_id), + path=f'v2/identities/{identity_id}/send-input', request_options=RequestOptions( method='post', body=encoder,