Skip to content

Commit 0fb0867

Browse files
authored
Merge pull request #67 from foarsitter/development
Async support for dossiers endpoint
2 parents c48bc8e + 33fd801 commit 0fb0867

File tree

7 files changed

+176
-43
lines changed

7 files changed

+176
-43
lines changed

poetry.lock

Lines changed: 21 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ typeguard = ">=2.13.3"
4949
xdoctest = {extras = ["colors"], version = ">=0.15.10"}
5050
myst-parser = {version = ">=0.16.1"}
5151
respx = "^0.20.1"
52+
pytest-asyncio = "^0.20.3"
5253

5354
[tool.coverage.paths]
5455
source = ["src", "*/site-packages"]

src/checkedid/client.py

Lines changed: 103 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from json import JSONDecodeError
2+
from types import TracebackType
23
from typing import Dict
34
from typing import List
45
from typing import Optional
@@ -8,28 +9,31 @@
89
import httpx
910
from httpx import Request
1011
from httpx import Response
12+
from httpx._types import URLTypes
1113

14+
from . import endpoints
15+
from . import errors
1216
from . import models
13-
from .errors import CheckedIDAuthenticationError
14-
from .errors import CheckedIDError
15-
from .errors import CheckedIDNotFoundError
16-
from .errors import CheckedIDValidationError
1717

1818

1919
_T = TypeVar("_T")
2020

2121

22-
class Client:
23-
ERROR_RESPONSE_MAPPING: Dict[int, Type[CheckedIDError]] = {
24-
422: CheckedIDValidationError,
25-
403: CheckedIDAuthenticationError,
26-
404: CheckedIDNotFoundError,
22+
class BaseClient:
23+
ERROR_RESPONSE_MAPPING: Dict[int, Type[errors.CheckedIDError]] = {
24+
422: errors.CheckedIDValidationError,
25+
403: errors.CheckedIDAuthenticationError,
26+
404: errors.CheckedIDNotFoundError,
2727
}
2828

2929
def __init__(self, customer_code: str, base_url: str = "https://api.checkedid.eu/"):
30-
self.httpx = httpx.Client(base_url=base_url, auth=self.authenticate_request)
30+
self.base_url = base_url
3131
self.access_token: Optional[str] = None
3232
self.customer_code = customer_code
33+
self.create_client(base_url)
34+
35+
def create_client(self, base_url: URLTypes) -> None:
36+
raise NotImplementedError
3337

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

4751
return None
4852

53+
def handle_error_response(self, response: Response) -> None:
54+
if response.status_code == 400:
55+
raise errors.CheckedIDValidationError(
56+
response.text, status_code=response.status_code
57+
)
58+
59+
try:
60+
json = response.json()
61+
except JSONDecodeError:
62+
json = {"message": response.text}
63+
64+
json["status_code"] = response.status_code
65+
66+
exception_type = self.map_exception(response)
67+
raise exception_type(
68+
status_code=response.status_code, json=json, message="Error from server"
69+
)
70+
71+
def map_exception(self, response: Response) -> Type[errors.CheckedIDError]:
72+
exception_type = self.ERROR_RESPONSE_MAPPING.get(
73+
response.status_code, errors.CheckedIDError
74+
)
75+
return exception_type
76+
77+
78+
class Client(BaseClient):
79+
client: httpx.Client
80+
4981
def oauth_token(
5082
self, grant_type: str, username: str, password: str
5183
) -> Optional[models.OAuthToken]:
52-
response = self.httpx.post(
84+
response = self.client.post(
5385
"/oauth/token",
5486
data={"grant_type": grant_type, "username": username, "password": password},
5587
)
@@ -62,8 +94,14 @@ def oauth_token(
6294
return typed_response
6395
return None
6496

97+
def __init__(self, customer_code: str, base_url: str = "https://api.checkedid.eu/"):
98+
super().__init__(customer_code, base_url)
99+
100+
def create_client(self, base_url: URLTypes) -> None:
101+
self.client = httpx.Client(base_url=base_url, auth=self.authenticate_request)
102+
65103
def invitation_status(self, invitation_code: str) -> Optional[models.Invitation]:
66-
response: Response = self.httpx.get(
104+
response: Response = self.client.get(
67105
f"/result/status/{invitation_code}",
68106
headers={"Accept": "application/json"},
69107
)
@@ -77,7 +115,7 @@ def invitations_create(
77115
CustomerCode=self.customer_code, Invitations=invitations
78116
)
79117

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

88126
def invitation_delete(self, invitation_code: str) -> bool:
89-
response: Response = self.httpx.delete(
127+
response: Response = self.client.delete(
90128
f"/invitation/{self.customer_code}/{invitation_code}",
91129
headers={"Accept": "application/json"},
92130
)
@@ -99,37 +137,69 @@ def invitation_delete(self, invitation_code: str) -> bool:
99137
return False
100138

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

104142
return self.process_response(response, models.ReportResponse)
105143

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

111149
return self.process_response(response, models.ReportDataV3)
112150

113-
def handle_error_response(self, response: Response) -> None:
114-
if response.status_code == 400:
115-
raise CheckedIDValidationError(
116-
response.text, status_code=response.status_code
117-
)
118151

119-
try:
120-
json = response.json()
121-
except JSONDecodeError:
122-
json = {"message": response.text}
152+
class ClientAsync(BaseClient):
153+
"""for asyncio"""
123154

124-
json["status_code"] = response.status_code
155+
client: httpx.AsyncClient
125156

126-
exception_type = self.map_exception(response)
127-
raise exception_type(
128-
status_code=response.status_code, json=json, message="Error from server"
157+
def __init__(self, customer_code: str, base_url: str = "https://api.checkedid.eu/"):
158+
super().__init__(customer_code, base_url)
159+
160+
async def oauth_token(
161+
self, grant_type: str, username: str, password: str
162+
) -> Optional[models.OAuthToken]:
163+
response = await self.client.post(
164+
"/oauth/token",
165+
data={"grant_type": grant_type, "username": username, "password": password},
129166
)
130167

131-
def map_exception(self, response: Response) -> Type[CheckedIDError]:
132-
exception_type = self.ERROR_RESPONSE_MAPPING.get(
133-
response.status_code, CheckedIDError
168+
typed_response = self.process_response(response, models.OAuthToken)
169+
170+
if typed_response:
171+
self.access_token = typed_response.access_token
172+
173+
return typed_response
174+
return None
175+
176+
def create_client(self, base_url: URLTypes) -> None:
177+
self.client = httpx.AsyncClient(base_url=base_url)
178+
179+
async def dossier(self, dossier_number: str) -> Optional[models.ReportResponse]:
180+
response = await self.client.get(
181+
url=endpoints.DossierEndpoint.url(dossier_number=dossier_number)
134182
)
135-
return exception_type
183+
184+
return self.process_response(response, endpoints.DossierEndpoint.response)
185+
186+
async def close(self) -> None:
187+
if self.client:
188+
await self.client.aclose()
189+
190+
def open(self) -> None:
191+
self.create_client(self.base_url)
192+
193+
async def __aenter__(self) -> "ClientAsync":
194+
"""Open the httpx client"""
195+
self.open()
196+
return self
197+
198+
async def __aexit__(
199+
self,
200+
exc_type: Optional[Type[BaseException]] = None,
201+
exc_value: Optional[BaseException] = None,
202+
traceback: Optional[TracebackType] = None,
203+
) -> None:
204+
"""Close the httpx client"""
205+
await self.close()

src/checkedid/endpoints.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from typing import Any
2+
from typing import Optional
3+
from typing import Type
4+
5+
from pydantic import BaseModel
6+
7+
from checkedid import models
8+
9+
10+
class Endpoint:
11+
path: str
12+
request: Optional[Type[BaseModel]]
13+
response = Optional[Type[BaseModel]]
14+
15+
16+
class DossierEndpoint(Endpoint):
17+
path = "/report/{dossier_number}"
18+
request = None
19+
response = models.ReportResponse
20+
21+
@classmethod
22+
def url(cls, **kwargs: Any) -> str:
23+
return cls.path.format(**kwargs)

tests/conftest.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,17 @@ def auth_client(client: Client, access_token_mock) -> Client:
5050
assert response.access_token
5151

5252
return client
53+
54+
55+
@pytest.fixture
56+
def dossier_number():
57+
return "999999-8888800"
58+
59+
60+
@pytest.fixture()
61+
def dossier_response_200(respx_mock, dossier_number):
62+
respx_mock.get("").mock(
63+
return_value=Response(
64+
status_code=200, json={"DossierNumber": dossier_number, "ReportPDF": ""}
65+
)
66+
)

tests/test_async_dossiers.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import pytest
2+
3+
from checkedid.client import ClientAsync
4+
5+
6+
@pytest.mark.asyncio
7+
async def test_dossiers(customer_code, dossier_response_200):
8+
client = ClientAsync(customer_code)
9+
10+
with client as client:
11+
response = await client.adossier("999999-8888800")
12+
13+
assert response.DossierNumber == "999999-8888800"

tests/test_dossiers.py

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,7 @@
44
from checkedid import errors
55

66

7-
def test_dossier(auth_client, respx_mock):
8-
dossier_number = "999999-8888800"
9-
respx_mock.get("").mock(
10-
return_value=Response(
11-
status_code=200, json={"DossierNumber": dossier_number, "ReportPDF": ""}
12-
)
13-
)
7+
def test_dossier(auth_client, respx_mock, dossier_response_200, dossier_number):
148
response = auth_client.dossier(dossier_number)
159

1610
assert response.DossierNumber == dossier_number

0 commit comments

Comments
 (0)