Skip to content

Commit

Permalink
Onfido client asyncio support
Browse files Browse the repository at this point in the history
  • Loading branch information
jerome-viveret-onfido committed Nov 9, 2023
1 parent 3d35380 commit 0930138
Show file tree
Hide file tree
Showing 21 changed files with 970 additions and 45 deletions.
1 change: 1 addition & 0 deletions onfido/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
from .onfido import Api
from .onfido import AsyncApi
132 changes: 132 additions & 0 deletions onfido/aio_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
from aiohttp import BufferedReaderPayload, ClientSession, MultipartWriter, StringPayload
from .exceptions import OnfidoRequestError
from aiohttp.client_reqrep import ClientResponse
from .onfido_download import OnfidoAioDownload
from .exceptions import async_error_decorator, OnfidoUnknownError
from .mimetype import mimetype_from_name
from .utils import form_data_converter
from typing import BinaryIO

try:
import importlib.metadata as importlib_metadata
except ImportError:
import importlib_metadata

CURRENT_VERSION = importlib_metadata.version("onfido-python")


class Resource:
def __init__(self, api_token, region, aio_session: ClientSession, timeout):
self._api_token = api_token
self._region = region
self._timeout = timeout
self._aio_session = aio_session

@property
def _url(self):
return getattr(self._region, "region_url", self._region)

def _build_url(self, path):
return self._url + path

@property
def _headers(self):
return {
"User-Agent": f"onfido-python/{CURRENT_VERSION}",
"Authorization": f"Token token={self._api_token}",
}

async def _handle_response(self, response: ClientResponse):
if response.status == 422:
error = None
if response.code == 422:
content_type = response.headers.get("Content-Type", "")
if "application/json" in content_type:
resp_json = await response.json()
error = resp_json.get("error")
raise OnfidoRequestError(error)

response.raise_for_status()
if response.status == 204:
return None

try:
return await response.json()
except ValueError as e:
raise OnfidoUnknownError("Onfido returned invalid JSON") from e

@async_error_decorator
async def _upload_request(self, path, file: BinaryIO, **request_body):
import uuid

boundary = uuid.uuid4().hex
with MultipartWriter("form-data", boundary=boundary) as mpwriter:
for k, v in form_data_converter(request_body).items():
part = mpwriter.append(
StringPayload(
v, headers={"Content-Disposition": f'form-data; name="{k}"'}
)
)

file_part = mpwriter.append_payload(
BufferedReaderPayload(
file,
content_type=mimetype_from_name(file.name),
headers={
"Content-Disposition": f'form-data; name="file"; filename="{file.name}"'
},
)
)
additional_headers = {
"Content-Type": f"multipart/form-data;boundary={boundary}"
}
async with self._aio_session.post(
self._build_url(path),
data=mpwriter,
headers=dict(self._headers, **additional_headers),
timeout=self._timeout,
) as response:
return await self._handle_response(response)

@async_error_decorator
async def _post(self, path, **request_body):
async with self._aio_session.post(
self._build_url(path),
json=request_body,
headers=self._headers,
timeout=self._timeout,
) as response:
return await self._handle_response(response)

@async_error_decorator
async def _put(self, path, data=None):
async with self._aio_session.put(
self._build_url(path),
json=data,
headers=self._headers,
timeout=self._timeout,
) as response:
return await self._handle_response(response)

@async_error_decorator
async def _get(self, path, payload=None):
async with self._aio_session.get(
self._build_url(path), headers=self._headers, timeout=self._timeout
) as response:
return await self._handle_response(response)

@async_error_decorator
async def _download_request(self, path):
async with self._aio_session.get(
self._build_url(path), headers=self._headers, timeout=self._timeout
) as response:
response.raise_for_status()

return OnfidoAioDownload(response)

@async_error_decorator
async def _delete_request(self, path):
async with self._aio_session.delete(
self._build_url(path), headers=self._headers, timeout=self._timeout
) as response:
return await self._handle_response(response)
43 changes: 40 additions & 3 deletions onfido/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,44 +1,58 @@
import requests
import requests.exceptions
from aiohttp.http_exceptions import HttpProcessingError
from aiohttp.client_exceptions import (
ServerTimeoutError,
ClientConnectionError,
ClientError,
)


class OnfidoError(Exception):
pass


class OnfidoRegionError(Exception):
pass


class OnfidoUnknownError(OnfidoError):
pass


class OnfidoInvalidSignatureError(OnfidoError):
pass


class OnfidoRequestError(OnfidoError):
pass


class OnfidoServerError(OnfidoError):
pass


class OnfidoConnectionError(OnfidoError):
pass


class OnfidoTimeoutError(OnfidoError):
pass


def error_decorator(func):
def wrapper(*args, **kwargs):
try:
return func(*args, **kwargs)
return func(*args, **kwargs)

except requests.HTTPError as e:
if e.response.status_code >= 500:
raise OnfidoServerError() from e
else:
error = None
if e.response.status_code == 422:
content_type = e.response.headers.get('Content-Type', '')
if 'application/json' in content_type:
content_type = e.response.headers.get("Content-Type", "")
if "application/json" in content_type:
resp_json = e.response.json()
error = resp_json.get("error")
raise OnfidoRequestError(error) from e
Expand All @@ -53,3 +67,26 @@ def wrapper(*args, **kwargs):
raise OnfidoUnknownError(e)

return wrapper


def async_error_decorator(func):
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)

except HttpProcessingError as e:
if e.code >= 500:
raise OnfidoServerError() from e
else:
raise OnfidoRequestError(e.message) from e

except ServerTimeoutError as e:
raise OnfidoTimeoutError(e)

except ClientConnectionError as e:
raise OnfidoConnectionError(e)

except ClientError as e:
raise OnfidoUnknownError(e)

return wrapper
32 changes: 32 additions & 0 deletions onfido/onfido.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,18 @@
from .regions import Region
from .exceptions import OnfidoRegionError

from .resources_aio.applicants import Applicants as AsyncApplicants
from .resources_aio.documents import Documents as AsyncDocuments
from .resources_aio.address_picker import Addresses as AsyncAddresses
from .resources_aio.checks import Checks as AsyncChecks
from .resources_aio.reports import Reports as AsyncReports
from .resources_aio.live_photos import LivePhotos as AsyncLivePhotos
from .resources_aio.live_videos import LiveVideos as AsyncLiveVideos
from .resources_aio.motion_captures import MotionCaptures as AsyncMotionCaptures
from .resources_aio.webhooks import Webhooks as AsyncWebhooks
from .resources_aio.sdk_tokens import SdkToken as AsyncSdkToken
from .resources_aio.extraction import Extraction as AsyncExtraction
from .resources_aio.watchlist_monitors import WatchlistMonitors as AsyncWatchlistMonitors

class Api:
def __init__(self, api_token, region, timeout=None):
Expand All @@ -33,3 +45,23 @@ def __init__(self, api_token, region, timeout=None):
pass
elif "api.onfido.com" in region:
raise OnfidoRegionError("The region must be one of Region.EU, Region.US or Region.CA. We previously defaulted to Region.EU, so if you previously didn’t set a region or used api.onfido.com, please set your region to Region.EU")

class AsyncApi:
def __init__(self, api_token, region, aio_session, timeout=None):
self.applicant = AsyncApplicants(api_token, region, aio_session, timeout)
self.document = AsyncDocuments(api_token, region, aio_session, timeout)
self.address = AsyncAddresses(api_token, region, aio_session, timeout)
self.check = AsyncChecks(api_token, region, aio_session, timeout)
self.report = AsyncReports(api_token, region, aio_session, timeout)
self.sdk_token = AsyncSdkToken(api_token, region, aio_session, timeout)
self.webhook = AsyncWebhooks(api_token, region, aio_session, timeout)
self.live_photo = AsyncLivePhotos(api_token, region, aio_session, timeout)
self.live_video = AsyncLiveVideos(api_token, region, aio_session, timeout)
self.motion_capture = AsyncMotionCaptures(api_token, region, aio_session, timeout)
self.extraction = AsyncExtraction(api_token, region, aio_session, timeout)
self.watchlist_monitor = AsyncWatchlistMonitors(api_token, region, aio_session, timeout)

if region in [Region.EU, Region.US, Region.CA]:
pass
elif "api.onfido.com" in region:
raise OnfidoRegionError("The region must be one of Region.EU, Region.US or Region.CA. We previously defaulted to Region.EU, so if you previously didn’t set a region or used api.onfido.com, please set your region to Region.EU")
7 changes: 7 additions & 0 deletions onfido/onfido_download.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
from aiohttp.client_reqrep import ClientResponse

class OnfidoDownload:
def __init__(self, response):
self.content = response.content
self.content_type = response.headers['content-type']

class OnfidoAioDownload:
def __init__(self, response: ClientResponse):
self.content = response.content
self.content_type = response.content_type
6 changes: 6 additions & 0 deletions onfido/resources_aio/address_picker.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ..aio_resource import Resource


class Addresses(Resource):
def pick(self, postcode: str):
return self._get(f"addresses/pick?postcode={postcode}")
23 changes: 23 additions & 0 deletions onfido/resources_aio/applicants.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from ..aio_resource import Resource


class Applicants(Resource):
def create(self, request_body: dict):
return self._post("applicants/", **request_body)

def update(self, applicant_id: str, request_body: dict):
return self._put(f"applicants/{applicant_id}", request_body)

def find(self, applicant_id: str):
return self._get(f"applicants/{applicant_id}")

def delete(self, applicant_id: str):
return self._delete_request(f"applicants/{applicant_id}")

def all(self, **user_payload: dict):
payload = {"include_deleted": False, "per_page": 20, "page": 1}
payload.update(user_payload)
return self._get("applicants", payload=payload)

def restore(self, applicant_id: str):
return self._post(f"applicants/{applicant_id}/restore")
19 changes: 19 additions & 0 deletions onfido/resources_aio/checks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
from ..aio_resource import Resource


class Checks(Resource):
def create(self, request_body: dict):
return self._post("checks/", **request_body)

def find(self, check_id: str):
return self._get(f"checks/{check_id}")

def all(self, applicant_id: str):
payload = {"applicant_id": applicant_id}
return self._get("checks/", payload=payload)

def resume(self, check_id: str):
return self._post(f"checks/{check_id}/resume")

def download(self, check_id: str):
return self._download_request(f"checks/{check_id}/download")
16 changes: 16 additions & 0 deletions onfido/resources_aio/documents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from ..aio_resource import Resource
from typing import BinaryIO


class Documents(Resource):
def upload(self, sample_file: BinaryIO, request_body):
return self._upload_request("documents/", sample_file, **request_body)

def find(self, document_id: str):
return self._get(f"documents/{document_id}")

def all(self, applicant_id: str):
return self._get(f"documents?applicant_id={applicant_id}")

def download(self, document_id: str):
return self._download_request(f"documents/{document_id}/download")
6 changes: 6 additions & 0 deletions onfido/resources_aio/extraction.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from ..aio_resource import Resource


class Extraction(Resource):
def perform(self, document_id: str):
return self._post("extractions/", document_id=document_id)
17 changes: 17 additions & 0 deletions onfido/resources_aio/live_photos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
from ..aio_resource import Resource
from typing import BinaryIO


class LivePhotos(Resource):
def upload(self, sample_file: BinaryIO, request_body):
return self._upload_request("live_photos/", sample_file, **request_body)

def find(self, live_photo_id: str):
return self._get(f"live_photos/{live_photo_id}")

def all(self, applicant_id: str):
payload = {"applicant_id": applicant_id}
return self._get("live_photos/", payload=payload)

def download(self, live_photo_id: str):
return self._download_request(f"live_photos/{live_photo_id}/download")
16 changes: 16 additions & 0 deletions onfido/resources_aio/live_videos.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from ..aio_resource import Resource


class LiveVideos(Resource):
def find(self, live_video_id: str):
return self._get(f"live_videos/{live_video_id}")

def all(self, applicant_id: str):
payload = {"applicant_id": applicant_id}
return self._get("live_videos/", payload=payload)

def download(self, live_video_id: str):
return self._download_request(f"live_videos/{live_video_id}/download")

def download_frame(self, live_video_id: str):
return self._download_request(f"live_videos/{live_video_id}/frame")
Loading

0 comments on commit 0930138

Please sign in to comment.