Skip to content

Commit

Permalink
Added dry run functionality per request
Browse files Browse the repository at this point in the history
  • Loading branch information
mmzeynalli committed Jan 27, 2025
1 parent 0ae8a61 commit 4332231
Show file tree
Hide file tree
Showing 4 changed files with 70 additions and 66 deletions.
12 changes: 8 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@

### What's Changed

#### Support

* Dropped support for Python3.8

#### New integrations

* Added KapitalBank integration

#### Fixes

* Added dry-run functionality per request class

#### Support

* Dropped support for Python3.8

## v2.0.1 (2024-10-28)

[GitHub release](https://github.com/mmzeynalli/integrify/releases/tag/v2.0.1)
Expand Down
68 changes: 30 additions & 38 deletions src/integrify/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def __init__(
sync: Sync (True) və ya Async (False) klient seçimi. Default olaraq sync seçilir.
"""
self.base_url = base_url
self.default_handler = default_handler or None
self.default_handler = default_handler or APIPayloadHandler(None, None)

self.request_executor = APIExecutor(name=name, sync=sync, dry=dry)
"""API sorğularını icra edən obyekt"""
Expand Down Expand Up @@ -108,16 +108,19 @@ class APIPayloadHandler:
def __init__(
self,
req_model: Optional[type[PayloadBaseModel]] = None,
resp_model: Optional[type[_ResponseT]] = None,
resp_model: Union[type[_ResponseT], type[dict], None] = dict,
dry: bool = False,
):
"""
Args:
req_model: Sorğunun payload model-i
resp_model: Sorğunun cavabının payload model-i
dry: Simulasiya bool-u: True olarsa, sorğu göndərilmir, göndərilən data qaytarılır
"""
self.req_model = req_model
self.__req_model: Optional[PayloadBaseModel] = None # initialized pydantic model
self.resp_model = resp_model
self.dry = dry

def set_urlparams(self, url: str) -> str:
"""URL-in query-param-larını set etmək üçün funksiya (əgər varsa)
Expand All @@ -140,7 +143,7 @@ def set_urlparams(self, url: str) -> str:
)
)

@property
@cached_property
def headers(self) -> dict:
"""Sorğunun header-ləri"""
return {}
Expand Down Expand Up @@ -204,14 +207,14 @@ def handle_request(self, *args, **kwds):
def handle_response(
self,
resp: httpx.Response,
) -> Union[APIResponse[_ResponseT], APIResponse[dict]]:
) -> Union[APIResponse[_ResponseT], APIResponse[dict], httpx.Response]:
"""Sorğudan gələn cavab payload-ı handle edən funksiya. `self.resp_model` schema-sı
verilibsə, onunla parse və validate olunur, əks halda, json/dict formatında qaytarılır.
"""
if self.resp_model:
return APIResponse[self.resp_model].model_validate(resp, from_attributes=True) # type: ignore[name-defined]
if not self.resp_model:
return resp

return APIResponse[dict].model_validate(resp, from_attributes=True)
return APIResponse[self.resp_model].model_validate(resp, from_attributes=True) # type: ignore[name-defined]


class APIExecutor:
Expand Down Expand Up @@ -242,7 +245,7 @@ def __init__(self, name: str, sync: bool = True, dry: bool = False):
def request_function(
self,
) -> Callable[
[str, str, Optional['APIPayloadHandler'], Any], # input args
[str, str, APIPayloadHandler, Any], # input args
Union[
Union[httpx.Response, APIResponse[_ResponseT], APIResponse[dict]],
Coroutine[
Expand All @@ -262,7 +265,7 @@ def sync_req(
self,
url: str,
verb: str,
handler: Optional['APIPayloadHandler'],
handler: APIPayloadHandler,
*args,
**kwds,
) -> Union[httpx.Response, APIResponse[_ResponseT], APIResponse[dict]]:
Expand All @@ -275,27 +278,24 @@ def sync_req(
"""
assert isinstance(self.client, httpx.Client)

data = handler.handle_request(*args, **kwds) if handler else None
headers = handler.headers if handler else None
full_url = handler.set_urlparams(url) if handler else url

if self.dry:
_type = type(data)
_data = json.dumps(data)
data = handler.handle_request(*args, **kwds)
headers = handler.headers
full_url = handler.set_urlparams(url)

return APIResponse[_type]( # type: ignore[valid-type, call-arg]
if self.dry or handler.dry:
return APIResponse[dict]( # type: ignore[valid-type, call-arg]
is_success=True,
status_code=200,
headers=headers or {},
content=_data,
content=json.dumps({**data, 'url': full_url}),
)

response = self.client.request(
verb,
full_url,
data=data,
headers=headers,
**(handler.req_args if handler else {}),
**handler.req_args,
)

if not response.is_success:
Expand All @@ -307,16 +307,13 @@ def sync_req(
response.content.decode(),
)

if handler:
return handler.handle_response(response)

return response
return handler.handle_response(response)

async def async_req( # pragma: no cover
self,
url: str,
verb: str,
handler: Optional['APIPayloadHandler'],
handler: APIPayloadHandler,
*args,
**kwds,
) -> Union[httpx.Response, APIResponse[_ResponseT], APIResponse[dict]]:
Expand All @@ -329,26 +326,24 @@ async def async_req( # pragma: no cover
"""
assert isinstance(self.client, httpx.AsyncClient)

data = handler.handle_request(*args, **kwds) if handler else None
headers = handler.headers if handler else None

if self.dry:
_type = type(data)
_data = json.dumps(data)
data = handler.handle_request(*args, **kwds)
headers = handler.headers
full_url = handler.set_urlparams(url)

return APIResponse[_type]( # type: ignore[valid-type, call-arg]
if self.dry: # Sorğu göndərmək əvəzinə göndəriləcək datanı qaytarmaq
return APIResponse[dict]( # type: ignore[valid-type, call-arg]
is_success=True,
status_code=200,
headers=headers or {},
content=_data,
content=json.dumps({**data, 'url': full_url}),
)

response = await self.client.request(
verb,
url,
full_url,
data=data,
headers=headers,
**(handler.req_args if handler else {}),
**handler.req_args,
)

if not response.is_success:
Expand All @@ -360,7 +355,4 @@ async def async_req( # pragma: no cover
response.content.decode(),
)

if handler:
return handler.handle_response(response)

return response
return handler.handle_response(response)
9 changes: 6 additions & 3 deletions src/integrify/kapital/handlers.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,17 +71,20 @@ def handle_response(self, resp: httpx.Response) -> APIResponse[_ResponseT]:
200-dən fərqli status kodu gələrsə, gələn cavabı modelə uyğunlaşdırır və error obyektini APIResponse obyektinə əlavə edir.
""" # noqa: E501

api_resp = APIResponse[BaseResponseSchema].model_validate(resp, from_attributes=True) # type: ignore[assignment]
api_resp = APIResponse[BaseResponseSchema].model_validate(resp, from_attributes=True)

if resp.status_code == 200:
if not self.resp_model:
raise ValueError('Response model is not set for this handler.')

data = self.get_response_data(resp.json())
api_resp.body.data = self.resp_model.model_validate(data, from_attributes=True) # type: ignore[attr-defined]

assert isinstance(self.resp_model, PayloadBaseModel)
api_resp.body.data = self.resp_model.model_validate(data, from_attributes=True)
else:
api_resp.body.error = ErrorResponseBodySchema.model_validate(
resp.json(), from_attributes=True
resp.json(),
from_attributes=True,
)

return api_resp # type: ignore[return-value]
Expand Down
47 changes: 26 additions & 21 deletions tests/test_base.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import json
from unittest.mock import patch

import httpx
import pytest
from httpx import Response
from pydantic import BaseModel
Expand Down Expand Up @@ -74,14 +74,14 @@ def __init__(self):
api_client.test('data1')


def test_missing_response_handler_input(
def test_default_response_handler_input(
api_client: APIClient,
test_ok_response,
mocker: MockerFixture,
):
class Handler(APIPayloadHandler):
def __init__(self):
super().__init__(RequestSchema, None)
super().__init__(req_model=RequestSchema)

with mocker.patch('httpx.Client.request', return_value=test_ok_response):
api_client.add_url('test', 'url', 'GET')
Expand All @@ -92,6 +92,24 @@ def __init__(self):
assert resp.body['data2'] == 'output2'


def test_none_response_handler_input(
api_client: APIClient,
test_ok_response,
mocker: MockerFixture,
):
class Handler(APIPayloadHandler):
def __init__(self):
super().__init__(req_model=RequestSchema, resp_model=None)

with mocker.patch('httpx.Client.request', return_value=test_ok_response):
api_client.add_url('test', 'url', 'GET')
api_client.add_handler('test', Handler)
resp = api_client.test(data1='input1')
assert isinstance(resp, httpx.Response)
assert resp.json()['data1'] == 'output1'
assert resp.json()['data2'] == 'output2'


def test_with_handlers(api_client: APIClient, test_ok_response, mocker: MockerFixture):
class Handler(APIPayloadHandler):
def __init__(self):
Expand Down Expand Up @@ -144,8 +162,9 @@ class Handler(APIPayloadHandler):
def __init__(self):
super().__init__(req_schema, ResponseSchema)

with mocker.patch('httpx.Client.request', return_value=test_ok_response), pytest.raises(
ValueError
with (
mocker.patch('httpx.Client.request', return_value=test_ok_response),
pytest.raises(ValueError),
):
api_client.add_url('test', 'url?q={data1}', 'GET')
api_client.add_handler('test', Handler)
Expand All @@ -155,7 +174,8 @@ def __init__(self):
def test_dry_run_none(dry_api_client):
dry_api_client.add_url('test', 'url', 'GET')
resp = dry_api_client.test()
assert isinstance(resp.body, type(None))
assert isinstance(resp.body, dict)
assert 'url' in resp.body


def test_dry_run_json(dry_api_client):
Expand All @@ -168,18 +188,3 @@ def __init__(self):
resp = dry_api_client.test(data1='input1')
assert isinstance(resp.body, dict)
assert resp.body['data1'] == 'input1'


def test_dry_run_dumped_json(dry_api_client):
class Handler(APIPayloadHandler):
def __init__(self):
super().__init__(RequestSchema, ResponseSchema)

def post_handle_payload(self, data):
return json.dumps(data)

dry_api_client.add_url('test', 'url', 'GET')
dry_api_client.add_handler('test', Handler)
resp = dry_api_client.test(data1='input1')
assert isinstance(resp.body, str)
assert resp.body == '{"data1": "input1"}'

0 comments on commit 4332231

Please sign in to comment.