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

Onfido client asyncio support #40

Merged
merged 1 commit into from
Nov 15, 2023
Merged
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
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)
jerive marked this conversation as resolved.
Show resolved Hide resolved
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)
jerive marked this conversation as resolved.
Show resolved Hide resolved

def update(self, applicant_id: str, request_body: dict):
return self._put(f"applicants/{applicant_id}", request_body)
jerive marked this conversation as resolved.
Show resolved Hide resolved

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)
jerive marked this conversation as resolved.
Show resolved Hide resolved

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
Loading