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 3, 2023
1 parent 3d35380 commit 10ea479
Show file tree
Hide file tree
Showing 21 changed files with 905 additions and 41 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
80 changes: 80 additions & 0 deletions onfido/aio_resource.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
from aiohttp import BytesIOPayload, ClientSession, MultipartWriter
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):
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):
with MultipartWriter('mixed') as mpwriter:
mpwriter.append_form(form_data_converter(request_body))
mpwriter.append_payload(BytesIOPayload(file, filename=file.name, content_type=mimetype_from_name(file.name)))
async with self._aio_session.post(self._build_url(path), data=mpwriter, headers=self._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)
31 changes: 30 additions & 1 deletion onfido/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import requests
import requests.exceptions

from aiohttp.http_exceptions import HttpProcessingError
from aiohttp.client_exceptions import ServerTimeoutError, ClientConnectionError, ClientError

class OnfidoError(Exception):
pass
Expand Down Expand Up @@ -53,3 +54,31 @@ 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:
error = None
# if e.code == 422:
# content_type = e.headers.get('Content-Type', '')
# if 'application/json' in content_type:
# resp_json = e.message
# error = resp_json.get("error")
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")
16 changes: 16 additions & 0 deletions onfido/resources_aio/motion_captures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from ..aio_resource import Resource


class MotionCaptures(Resource):
def find(self, motion_capture_id:str):
return self._get(f"motion_captures/{motion_capture_id}")

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

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

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


class Reports(Resource):
def find(self, report_id:str):
return self._get(f"reports/{report_id}")

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

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

def cancel(self, report_id:str):
return self._post(f"reports/{report_id}/cancel")
5 changes: 5 additions & 0 deletions onfido/resources_aio/sdk_tokens.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from ..aio_resource import Resource

class SdkToken(Resource):
def generate(self, request_body):
return self._post("sdk_token", **request_body)
16 changes: 16 additions & 0 deletions onfido/resources_aio/watchlist_monitors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from ..aio_resource import Resource


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

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

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

def delete(self, monitor_id:str):
return self._delete_request(f"watchlist_monitors/{monitor_id}")
18 changes: 18 additions & 0 deletions onfido/resources_aio/webhooks.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from ..aio_resource import Resource


class Webhooks(Resource):
def create(self, request_body:dict):
return self._post("webhooks", **request_body)

def all(self):
return self._get("webhooks")

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

def edit(self, webhook_id:str, request_body:dict):
return self._put(f"webhooks/{webhook_id}", request_body)

def delete(self, webhook_id:str):
return self._delete_request(f"webhooks/{webhook_id}")
Loading

0 comments on commit 10ea479

Please sign in to comment.