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

[WIP] Implement api services #9

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
SHELL := bash
PATH := ./venv/bin:${PATH}
PYTHON = python3.7
PYTHON = python3
PROJECT = mati
isort = isort -rc -ac $(PROJECT) tests setup.py
black = black -S -l 79 --target-version py37 $(PROJECT) tests setup.py
black = black -S -l 79 --target-version py36 $(PROJECT) tests setup.py


all: test
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ pip install mati

```
make venv
source venv/bin/active
source venv/bin/activate
make test
```

Expand Down
7 changes: 6 additions & 1 deletion mati/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
__all__ = ['Client', '__version__']
__all__ = ['ApiService', 'ApiServiceV1', 'api_service', 'api_service_v1', 'Client', '__version__']

from .client import Client
from .api_service import ApiService
from .api_service_v1 import ApiServiceV1
from .version import __version__

api_service = ApiService()
api_service_v1 = ApiServiceV1()
127 changes: 127 additions & 0 deletions mati/api_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
import hashlib
import hmac
from base64 import b64encode
from typing import Optional

from mati.call_http import RequestOptions, call_http, ErrorResponse
from mati.types import AuthType, IdentityMetadata, IdentityResource

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


class ApiService:
bearer_auth_header: str = None
client_auth_header: str = None
host: str
webhook_secret: Optional[str]

def init(self, client_id: str, client_secret: str, webhook_secret: str = None, host: str = None):
"""
Initializes the service. Call this method before using api calls.
---------
:param str client_id:
:param str client_secret:
:param str host:
:param str webhook_secret:
:return: None
"""
self.host = host or API_HOST
self.webhook_secret = webhook_secret or None
self._set_client_auth(client_id, client_secret)

def validate_signature(self, signature: str, body_str: str):
"""
Validates signature of requests.
We use webhookSecret to sign data. You put this value in the Dashboard, when define webhooks.
And provide the same value, when you initialize the service. Please, use a strong secret value.
Draw your attention. The order of the fields in the body is important. Keep the original one.
---------
:param str signature: signature from x-signature header of request calculated on Mati side
:param str body_str: data came in request body
:raise: Exception if `webhook_secret` is not provided
:return: bool `true` if the signature is valid, `false` - otherwise
"""
if self.webhook_secret is None:
raise Exception('No webhookSecret provided')
dig = hmac.new(self.webhook_secret.encode(), msg=body_str.encode(), digestmod=hashlib.sha256)
return signature == dig.hexdigest()

def fetch_resource(self, url: str):
"""
Fetches resource by its absolute URL using your client credentials you provide,
when you initialize the service. Usually you do not need to build url by yourself,
its values come in webhooks or in other resources.

:param url: absolute url of the resource
:return: resource
:raise ErrorResponse if we get http error
"""
return self._call_http(url=url)

def create_identity(self, metadata: IdentityMetadata) -> IdentityResource:
"""
Starts new verification flow and creates identity. You should use result of this method
in order to get id for further `#sendInput` calls.

:param {IdentityMetadata} metadata: payload you want to pass to the identity
:return: resource of identity created.
:raise ErrorResponse if we get http error
"""
return self._call_http(
path='v2/identities',
request_options=RequestOptions(
method='post',
body={'metadata': metadata}
)
)

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')

def _set_bearer_auth(self, access_token):
self.bearer_auth_header = f'Bearer {access_token}'

def auth(self):
auth_response = self._call_http(
path='oauth',
auth_type=AuthType.basic,
request_options=RequestOptions(
method='post',
body={'grant_type': 'client_credentials'},
headers={'content-type': 'application/x-www-form-urlencoded'},
)
)
self._set_bearer_auth(auth_response['access_token'])
return auth_response

def _call_http(
self,
path: str = None,
url: str = None,
request_options: RequestOptions = RequestOptions(),
auth_type: AuthType = AuthType.bearer,
):
tried_auth = False
if auth_type == AuthType.bearer and self.bearer_auth_header is None:
self.auth()
tried_auth = True
if auth_type != AuthType.none:
authorization = None
if auth_type == AuthType.bearer:
authorization = self.bearer_auth_header
elif auth_type == AuthType.basic:
authorization = self.client_auth_header
if authorization is not None:
headers = request_options.headers or dict()
headers['Authorization'] = authorization
request_options.headers = headers
request_url = url or f'{self.host}/{path}'
try:
return call_http(request_url, request_options)
except ErrorResponse as err:
if not tried_auth and auth_type == AuthType.bearer and err.response.status_code == 401:
self.auth()
request_options.headers['Authorization'] = self.bearer_auth_header
return call_http(request_url, request_options)
raise err
110 changes: 110 additions & 0 deletions mati/api_service_v1.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import hashlib
import hmac
from base64 import b64encode
from typing import Optional

from mati.call_http import RequestOptions, call_http, ErrorResponse
from mati.types import AuthType

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


class ApiServiceV1:
bearer_auth_header: str = None
client_auth_header: str = None
host: str
webhook_secret: Optional[str]

def init(self, client_id: str, client_secret: str, webhook_secret: str = None, host: str = None):
"""
Initializes the service. Call this method before using api calls.
---------
:param str client_id:
:param str client_secret:
:param str host:
:param str webhook_secret:
:return: None
"""
self.host = host or API_HOST
self.webhook_secret = webhook_secret or None
self._set_client_auth(client_id, client_secret)

def validate_signature(self, signature: str, body_str: str):
"""
Validates signature of requests.
We use webhookSecret to sign data. You put this value in the Dashboard, when define webhooks.
And provide the same value, when you initialize the service. Please, use a strong secret value.
Draw your attention. The order of the fields in the body is important. Keep the original one.
---------
:param str signature: signature from x-signature header of request calculated on Mati side
:param str body_str: data came in request body
:raise: Exception if `webhook_secret` is not provided
:return: bool `true` if the signature is valid, `false` - otherwise
"""
if self.webhook_secret is None:
raise Exception('No webhookSecret provided')
dig = hmac.new(self.webhook_secret.encode(), msg=body_str.encode(), digestmod=hashlib.sha256)
return signature == dig.hexdigest()

def fetch_resource(self, url: str):
"""
Fetches resource by its absolute URL using your client credentials you provide,
when you initialize the service. Usually you do not need to build url by yourself,
its values come in webhooks or in other resources.

:param url:
:return: resource
:raise ErrorResponse if we get http error
"""
return self._call_http(url=url)

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')

def _set_bearer_auth(self, access_token):
self.bearer_auth_header = f'Bearer {access_token}'

def auth(self):
auth_response = self._call_http(
path='oauth/token',
auth_type=AuthType.basic,
request_options=RequestOptions(
method='post',
body={'grant_type': 'client_credentials', 'scope': 'identity' },
headers={'content-type': 'application/x-www-form-urlencoded'},
)
)
self._set_bearer_auth(auth_response['access_token'])
return auth_response

def _call_http(
self,
path: str = None,
url: str = None,
request_options: RequestOptions = RequestOptions(),
auth_type: AuthType = AuthType.bearer,
):
tried_auth = False
if auth_type == AuthType.bearer and self.bearer_auth_header is None:
self.auth()
tried_auth = True
if auth_type != AuthType.none:
authorization = None
if auth_type == AuthType.bearer:
authorization = self.bearer_auth_header
elif auth_type == AuthType.basic:
authorization = self.client_auth_header
if authorization is not None:
headers = request_options.headers or dict()
headers['Authorization'] = authorization
request_options.headers = headers
request_url = url or f'{self.host}/{path}'
try:
return call_http(request_url, request_options)
except ErrorResponse as err:
if not tried_auth and auth_type == AuthType.bearer and err.response.status_code == 401:
self.auth()
request_options.headers['Authorization'] = self.bearer_auth_header
return call_http(request_url, request_options)
raise err
26 changes: 26 additions & 0 deletions mati/call_http.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from typing import Dict

from attr import dataclass
from requests import Session, Response

session = Session()


@dataclass
class RequestOptions:
method: str = 'get'
headers: Dict[str, str] = None
body: Dict = None


class ErrorResponse(Exception):
message: str
response: Response


def call_http(request_url: str, request_options: RequestOptions):
response = session.request(request_options.method, request_url, headers=request_options.headers)
if not response.ok:
print(f'response.text: {response.text}')
raise ErrorResponse(response.text, response)
return response.json()
4 changes: 2 additions & 2 deletions mati/resources/identities.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from dataclasses import dataclass, field
from typing import ClassVar, List, Optional, Union

from ..types import UserValidationFile
from ..types import UserValidationFile, IdentityMetadata
from .base import Resource
from .user_verification_data import UserValidationData

Expand All @@ -22,7 +22,7 @@ class Identity(Resource):
status: str
annotatedStatus: Optional[str] = None
user: Optional[str] = None
metadata: Union[dict, List[str]] = field(default_factory=dict)
metadata: IdentityMetadata = field(default_factory=dict)
fullName: Optional[str] = None
facematchScore: Optional[float] = None

Expand Down
43 changes: 43 additions & 0 deletions mati/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,3 +54,46 @@ class UserValidationFile:
region: str = '' # 2-digit US State code (if applicable)
group: int = 0
page: Union[str, PageType] = PageType.front


##########

IdentityMetadata = Union[dict, List[str]]

@dataclass
class IdentityStatusTypes(SerializableEnum):
deleted = 'deleted',
pending = 'pending',
rejected = 'rejected',
review_needed = 'reviewNeeded',
running = 'running',
verified = 'verified',


@dataclass
class IdentityResource:
id: str
status: IdentityStatusTypes


@dataclass
class EventNameTypes(SerializableEnum):
step_completed = 'step_completed',
verification_completed = 'verification_completed',
verification_expired = 'verification_expired',
verification_inputs_completed = 'verification_inputs_completed',
verification_started = 'verification_started',
verification_updated = 'verification_updated',


@dataclass
class WebhookResource:
eventName: EventNameTypes
metadata: IdentityMetadata
resource: str


class AuthType(SerializableEnum):
bearer = 'bearer'
basic = 'basic'
none = 'none'
Loading