Skip to content

Commit

Permalink
Merge pull request #67 from foarsitter/development
Browse files Browse the repository at this point in the history
Async support for dossiers endpoint
  • Loading branch information
foarsitter authored Jan 23, 2023
2 parents c48bc8e + 33fd801 commit 0fb0867
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 43 deletions.
24 changes: 21 additions & 3 deletions poetry.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ typeguard = ">=2.13.3"
xdoctest = {extras = ["colors"], version = ">=0.15.10"}
myst-parser = {version = ">=0.16.1"}
respx = "^0.20.1"
pytest-asyncio = "^0.20.3"

[tool.coverage.paths]
source = ["src", "*/site-packages"]
Expand Down
136 changes: 103 additions & 33 deletions src/checkedid/client.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from json import JSONDecodeError
from types import TracebackType
from typing import Dict
from typing import List
from typing import Optional
Expand All @@ -8,28 +9,31 @@
import httpx
from httpx import Request
from httpx import Response
from httpx._types import URLTypes

from . import endpoints
from . import errors
from . import models
from .errors import CheckedIDAuthenticationError
from .errors import CheckedIDError
from .errors import CheckedIDNotFoundError
from .errors import CheckedIDValidationError


_T = TypeVar("_T")


class Client:
ERROR_RESPONSE_MAPPING: Dict[int, Type[CheckedIDError]] = {
422: CheckedIDValidationError,
403: CheckedIDAuthenticationError,
404: CheckedIDNotFoundError,
class BaseClient:
ERROR_RESPONSE_MAPPING: Dict[int, Type[errors.CheckedIDError]] = {
422: errors.CheckedIDValidationError,
403: errors.CheckedIDAuthenticationError,
404: errors.CheckedIDNotFoundError,
}

def __init__(self, customer_code: str, base_url: str = "https://api.checkedid.eu/"):
self.httpx = httpx.Client(base_url=base_url, auth=self.authenticate_request)
self.base_url = base_url
self.access_token: Optional[str] = None
self.customer_code = customer_code
self.create_client(base_url)

def create_client(self, base_url: URLTypes) -> None:
raise NotImplementedError

def authenticate_request(self, request: Request) -> Request:
if self.access_token:
Expand All @@ -46,10 +50,38 @@ def process_response(

return None

def handle_error_response(self, response: Response) -> None:
if response.status_code == 400:
raise errors.CheckedIDValidationError(
response.text, status_code=response.status_code
)

try:
json = response.json()
except JSONDecodeError:
json = {"message": response.text}

json["status_code"] = response.status_code

exception_type = self.map_exception(response)
raise exception_type(
status_code=response.status_code, json=json, message="Error from server"
)

def map_exception(self, response: Response) -> Type[errors.CheckedIDError]:
exception_type = self.ERROR_RESPONSE_MAPPING.get(
response.status_code, errors.CheckedIDError
)
return exception_type


class Client(BaseClient):
client: httpx.Client

def oauth_token(
self, grant_type: str, username: str, password: str
) -> Optional[models.OAuthToken]:
response = self.httpx.post(
response = self.client.post(
"/oauth/token",
data={"grant_type": grant_type, "username": username, "password": password},
)
Expand All @@ -62,8 +94,14 @@ def oauth_token(
return typed_response
return None

def __init__(self, customer_code: str, base_url: str = "https://api.checkedid.eu/"):
super().__init__(customer_code, base_url)

def create_client(self, base_url: URLTypes) -> None:
self.client = httpx.Client(base_url=base_url, auth=self.authenticate_request)

def invitation_status(self, invitation_code: str) -> Optional[models.Invitation]:
response: Response = self.httpx.get(
response: Response = self.client.get(
f"/result/status/{invitation_code}",
headers={"Accept": "application/json"},
)
Expand All @@ -77,7 +115,7 @@ def invitations_create(
CustomerCode=self.customer_code, Invitations=invitations
)

response: Response = self.httpx.post(
response: Response = self.client.post(
"/invitations",
json=obj.dict(),
headers={"Accept": "application/json", "Content-Type": "application/json"},
Expand All @@ -86,7 +124,7 @@ def invitations_create(
return self.process_response(response, models.CustomerDetails)

def invitation_delete(self, invitation_code: str) -> bool:
response: Response = self.httpx.delete(
response: Response = self.client.delete(
f"/invitation/{self.customer_code}/{invitation_code}",
headers={"Accept": "application/json"},
)
Expand All @@ -99,37 +137,69 @@ def invitation_delete(self, invitation_code: str) -> bool:
return False

def dossier(self, dossier_number: str) -> Optional[models.ReportResponse]:
response = self.httpx.get(f"/report/{dossier_number}")
response = self.client.get(f"/report/{dossier_number}")

return self.process_response(response, models.ReportResponse)

def dossier_with_scope(
self, dossier_number: str, scope: str
) -> Optional[models.ReportDataV3]:
response = self.httpx.get(f"/reportdata/{dossier_number}/{scope}")
response = self.client.get(f"/reportdata/{dossier_number}/{scope}")

return self.process_response(response, models.ReportDataV3)

def handle_error_response(self, response: Response) -> None:
if response.status_code == 400:
raise CheckedIDValidationError(
response.text, status_code=response.status_code
)

try:
json = response.json()
except JSONDecodeError:
json = {"message": response.text}
class ClientAsync(BaseClient):
"""for asyncio"""

json["status_code"] = response.status_code
client: httpx.AsyncClient

exception_type = self.map_exception(response)
raise exception_type(
status_code=response.status_code, json=json, message="Error from server"
def __init__(self, customer_code: str, base_url: str = "https://api.checkedid.eu/"):
super().__init__(customer_code, base_url)

async def oauth_token(
self, grant_type: str, username: str, password: str
) -> Optional[models.OAuthToken]:
response = await self.client.post(
"/oauth/token",
data={"grant_type": grant_type, "username": username, "password": password},
)

def map_exception(self, response: Response) -> Type[CheckedIDError]:
exception_type = self.ERROR_RESPONSE_MAPPING.get(
response.status_code, CheckedIDError
typed_response = self.process_response(response, models.OAuthToken)

if typed_response:
self.access_token = typed_response.access_token

return typed_response
return None

def create_client(self, base_url: URLTypes) -> None:
self.client = httpx.AsyncClient(base_url=base_url)

async def dossier(self, dossier_number: str) -> Optional[models.ReportResponse]:
response = await self.client.get(
url=endpoints.DossierEndpoint.url(dossier_number=dossier_number)
)
return exception_type

return self.process_response(response, endpoints.DossierEndpoint.response)

async def close(self) -> None:
if self.client:
await self.client.aclose()

def open(self) -> None:
self.create_client(self.base_url)

async def __aenter__(self) -> "ClientAsync":
"""Open the httpx client"""
self.open()
return self

async def __aexit__(
self,
exc_type: Optional[Type[BaseException]] = None,
exc_value: Optional[BaseException] = None,
traceback: Optional[TracebackType] = None,
) -> None:
"""Close the httpx client"""
await self.close()
23 changes: 23 additions & 0 deletions src/checkedid/endpoints.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
from typing import Any
from typing import Optional
from typing import Type

from pydantic import BaseModel

from checkedid import models


class Endpoint:
path: str
request: Optional[Type[BaseModel]]
response = Optional[Type[BaseModel]]


class DossierEndpoint(Endpoint):
path = "/report/{dossier_number}"
request = None
response = models.ReportResponse

@classmethod
def url(cls, **kwargs: Any) -> str:
return cls.path.format(**kwargs)
14 changes: 14 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,17 @@ def auth_client(client: Client, access_token_mock) -> Client:
assert response.access_token

return client


@pytest.fixture
def dossier_number():
return "999999-8888800"


@pytest.fixture()
def dossier_response_200(respx_mock, dossier_number):
respx_mock.get("").mock(
return_value=Response(
status_code=200, json={"DossierNumber": dossier_number, "ReportPDF": ""}
)
)
13 changes: 13 additions & 0 deletions tests/test_async_dossiers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import pytest

from checkedid.client import ClientAsync


@pytest.mark.asyncio
async def test_dossiers(customer_code, dossier_response_200):
client = ClientAsync(customer_code)

with client as client:
response = await client.adossier("999999-8888800")

assert response.DossierNumber == "999999-8888800"
8 changes: 1 addition & 7 deletions tests/test_dossiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,7 @@
from checkedid import errors


def test_dossier(auth_client, respx_mock):
dossier_number = "999999-8888800"
respx_mock.get("").mock(
return_value=Response(
status_code=200, json={"DossierNumber": dossier_number, "ReportPDF": ""}
)
)
def test_dossier(auth_client, respx_mock, dossier_response_200, dossier_number):
response = auth_client.dossier(dossier_number)

assert response.DossierNumber == dossier_number
Expand Down

0 comments on commit 0fb0867

Please sign in to comment.