Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement send input action #12

Open
wants to merge 6 commits into
base: refactoring
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 29 additions & 2 deletions mati/api_service.py
Original file line number Diff line number Diff line change
@@ -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, InputsData

API_HOST = 'https://api.getmati.com'

Expand Down Expand Up @@ -75,6 +77,31 @@ def create_identity(self, metadata: IdentityMetadata) -> IdentityResource:
)
)

def send_input(self, identity_id: str, inputs_data: InputsData) -> List[Dict[str, bool]]:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we have class name and its structure like we have in js lib (SendInputRequest)?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

"""
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'
return self._call_http(
path=endpoint.format(identity_id=identity_id),

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

f-string would look nicer here :) like this:

...
path=f'v2/identities/{identity_id}/send-input',
...

Copy link
Contributor Author

@Ashhm Ashhm Jan 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cool! Thanks, done

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')
Expand Down
12 changes: 9 additions & 3 deletions mati/call_http.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,7 +11,7 @@
class RequestOptions:
method: str = 'get'
headers: Dict[str, str] = None
body: Dict = None
body: Union[Dict, MultipartEncoder] = None


class ErrorResponse(Exception):
Expand All @@ -19,7 +20,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.body,
)
if not response.ok:
print(f'response.text: {response.text}')
raise ErrorResponse(response.text, response)
Expand Down
4 changes: 2 additions & 2 deletions mati/resources/user_verification_data.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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,
Expand Down
4 changes: 2 additions & 2 deletions mati/resources/verifications.py
Original file line number Diff line number Diff line change
@@ -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


Expand All @@ -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
Expand Down
88 changes: 73 additions & 15 deletions mati/types.py
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -13,21 +13,21 @@ 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'
proof_of_residency = 'proof-of-residency'


@dataclass
class VerificationDocumentStep:
class VerificationStep:
id: str
status: int
error: Optional[str] = None
Expand All @@ -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

Expand All @@ -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
Expand All @@ -60,20 +60,78 @@ class UserValidationFile:

IdentityMetadata = Union[dict, List[str]]


@dataclass
class MediaInputOptions:
fieldName: str
fileData: Union[BinaryIO, str]


@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(dict):
def __init__(
self,
input_type: VerificationInputType,
data: Union[PhotoInputData, SelfiePhotoInputData, SelfieVideoInputData],
group: int = None
):
if group is not None:
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure about this, does there any other way to not pass group if is None?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this looks ok. you can also add group key after initializing the required keys to avoid code duplication I guess

Copy link
Contributor Author

@Ashhm Ashhm Jan 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But I will have to add group property for every instance?

dict.__init__(

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

any reason not to use super() here and in the else branch?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to serialize this structure with json.dumps in the future so I inherited from dict. There are two different structures with the group and without and if group equals None it converts to null and it's not correct for our case, so I used super constructor to handle such case

self,
input_type=input_type,
data=asdict(data),
group=group,
)
else:
dict.__init__(
self,
input_type=input_type,
data=asdict(data),
)


@dataclass
class InputsData:
files: List[MediaInputOptions]
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
Expand Down
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
79 changes: 79 additions & 0 deletions tests/cassettes/test_api_service_send_input.yaml
Original file line number Diff line number Diff line change
@@ -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
14 changes: 7 additions & 7 deletions tests/resources/test_user_verification_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
from mati.types import (
PageType,
UserValidationFile,
ValidationInputType,
ValidationType,
VerificationInputType,
DocumentType,
)

FIXTURE_DIR = os.path.join(
Expand All @@ -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(
[
Expand Down
Loading