Skip to content

Commit 9de08aa

Browse files
committed
Extract Create and Delete mixins as Agreements can't be created or deleted
1 parent 402e723 commit 9de08aa

File tree

6 files changed

+148
-112
lines changed

6 files changed

+148
-112
lines changed

mpt_api_client/http/async_service.py

Lines changed: 29 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -78,17 +78,6 @@ async def iterate(self, batch_size: int = 100) -> AsyncIterator[Model]:
7878
break
7979
offset = items_collection.meta.pagination.next_offset()
8080

81-
async def create(self, resource_data: ResourceData) -> Model:
82-
"""Create a new resource using `POST /endpoint`.
83-
84-
Returns:
85-
New resource created.
86-
"""
87-
response = await self.http_client.post(self._endpoint, json=resource_data)
88-
response.raise_for_status()
89-
90-
return self._model_class.from_response(response)
91-
9281
async def get(self, resource_id: str, select: list[str] | str | None = None) -> Model:
9382
"""Fetch a specific resource using `GET /endpoint/{resource_id}`.
9483
@@ -116,16 +105,6 @@ async def update(self, resource_id: str, resource_data: ResourceData) -> Model:
116105
"""
117106
return await self._resource_action(resource_id, "PUT", json=resource_data)
118107

119-
async def delete(self, resource_id: str) -> None:
120-
"""Delete resource using `DELETE /endpoint/{resource_id}`.
121-
122-
Args:
123-
resource_id: Resource ID.
124-
"""
125-
url = urljoin(f"{self._endpoint}/", resource_id)
126-
response = await self.http_client.delete(url)
127-
response.raise_for_status()
128-
129108
async def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
130109
"""Fetch one page of resources.
131110
@@ -193,3 +172,32 @@ async def _resource_action(
193172
resource_id, method, action, json=json, query_params=query_params
194173
)
195174
return self._model_class.from_response(response)
175+
176+
177+
class AsyncCreateMixin[Model: BaseModel](AsyncService[Model]):
178+
"""Create resource mixin."""
179+
180+
async def create(self, resource_data: ResourceData) -> Model:
181+
"""Create a new resource using `POST /endpoint`.
182+
183+
Returns:
184+
New resource created.
185+
"""
186+
response = await self.http_client.post(self._endpoint, json=resource_data)
187+
response.raise_for_status()
188+
189+
return self._model_class.from_response(response)
190+
191+
192+
class AsyncDeleteMixin[Model: BaseModel](AsyncService[Model]):
193+
"""Delete resource mixin."""
194+
195+
async def delete(self, resource_id: str) -> None:
196+
"""Delete resource using `DELETE /endpoint/{resource_id}`.
197+
198+
Args:
199+
resource_id: Resource ID.
200+
"""
201+
url = urljoin(f"{self._endpoint}/", resource_id)
202+
response = await self.http_client.delete(url)
203+
response.raise_for_status()

mpt_api_client/http/service.py

Lines changed: 28 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -77,17 +77,6 @@ def iterate(self, batch_size: int = 100) -> Iterator[Model]:
7777
break
7878
offset = items_collection.meta.pagination.next_offset()
7979

80-
def create(self, resource_data: ResourceData) -> Model:
81-
"""Create a new resource using `POST /endpoint`.
82-
83-
Returns:
84-
New resource created.
85-
"""
86-
response = self.http_client.post(self._endpoint, json=resource_data)
87-
response.raise_for_status()
88-
89-
return self._model_class.from_response(response)
90-
9180
def get(self, resource_id: str, select: list[str] | str | None = None) -> Model:
9281
"""Fetch a specific resource using `GET /endpoint/{resource_id}`.
9382
@@ -116,15 +105,6 @@ def update(self, resource_id: str, resource_data: ResourceData) -> Model:
116105
"""
117106
return self._resource_action(resource_id, "PUT", json=resource_data)
118107

119-
def delete(self, resource_id: str) -> None:
120-
"""Delete resource using `DELETE /endpoint/{resource_id}`.
121-
122-
Args:
123-
resource_id: Resource ID.
124-
"""
125-
response = self._resource_do_request(resource_id, "DELETE")
126-
response.raise_for_status()
127-
128108
def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Response:
129109
"""Fetch one page of resources.
130110
@@ -192,3 +172,31 @@ def _resource_action(
192172
resource_id, method, action, json=json, query_params=query_params
193173
)
194174
return self._model_class.from_response(response)
175+
176+
177+
class CreateMixin[Model: BaseModel](Service[Model]):
178+
"""Create resource mixin."""
179+
180+
def create(self, resource_data: ResourceData) -> Model:
181+
"""Create a new resource using `POST /endpoint`.
182+
183+
Returns:
184+
New resource created.
185+
"""
186+
response = self.http_client.post(self._endpoint, json=resource_data)
187+
response.raise_for_status()
188+
189+
return self._model_class.from_response(response)
190+
191+
192+
class DeleteMixin[Model: BaseModel](Service[Model]):
193+
"""Delete resource mixin."""
194+
195+
def delete(self, resource_id: str) -> None:
196+
"""Delete resource using `DELETE /endpoint/{resource_id}`.
197+
198+
Args:
199+
resource_id: Resource ID.
200+
"""
201+
response = self._resource_do_request(resource_id, "DELETE")
202+
response.raise_for_status()

mpt_api_client/resources/commerce/orders.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from mpt_api_client.http import AsyncService, Service
2+
from mpt_api_client.http.async_service import AsyncCreateMixin, AsyncDeleteMixin
3+
from mpt_api_client.http.service import CreateMixin, DeleteMixin
24
from mpt_api_client.models import Model, ResourceData
35

46

@@ -14,7 +16,12 @@ class OrdersServiceConfig:
1416
_collection_key = "data"
1517

1618

17-
class OrdersService(Service[Order], OrdersServiceConfig):
19+
class OrdersService( # noqa: WPS215
20+
CreateMixin[Order],
21+
DeleteMixin[Order],
22+
Service[Order],
23+
OrdersServiceConfig,
24+
):
1825
"""Orders service."""
1926

2027
def validate(self, resource_id: str, resource_data: ResourceData | None = None) -> Order:
@@ -84,7 +91,12 @@ def template(self, resource_id: str) -> str:
8491
return response.text
8592

8693

87-
class AsyncOrdersService(AsyncService[Order], OrdersServiceConfig):
94+
class AsyncOrdersService( # noqa: WPS215
95+
AsyncCreateMixin[Order],
96+
AsyncDeleteMixin[Order],
97+
AsyncService[Order],
98+
OrdersServiceConfig,
99+
):
88100
"""Async Orders service."""
89101

90102
async def validate(self, resource_id: str, resource_data: ResourceData | None = None) -> Order:

tests/http/conftest.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,25 @@
22
import pytest
33

44
from mpt_api_client import RQLQuery
5-
from mpt_api_client.http.async_service import AsyncService
6-
from mpt_api_client.http.service import Service
5+
from mpt_api_client.http.async_service import AsyncCreateMixin, AsyncDeleteMixin, AsyncService
6+
from mpt_api_client.http.service import CreateMixin, DeleteMixin, Service
77
from tests.conftest import DummyModel
88

99

10-
class DummyService(Service[DummyModel]):
10+
class DummyService(
11+
CreateMixin[DummyModel],
12+
DeleteMixin[DummyModel],
13+
Service[DummyModel],
14+
):
1115
_endpoint = "/api/v1/test"
1216
_model_class = DummyModel
1317

1418

15-
class AsyncDummyService(AsyncService[DummyModel]):
19+
class AsyncDummyService(
20+
AsyncCreateMixin[DummyModel],
21+
AsyncDeleteMixin[DummyModel],
22+
AsyncService[DummyModel],
23+
):
1624
_endpoint = "/api/v1/test"
1725
_model_class = DummyModel
1826

tests/http/test_async_service.py

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,39 @@
88
from tests.http.conftest import AsyncDummyService
99

1010

11+
async def test_async_create_mixin(async_dummy_service): # noqa: WPS210
12+
resource_data = {"name": "Test Resource", "status": "active"}
13+
new_resource_data = {"id": "new-resource-id", "name": "Test Resource", "status": "active"}
14+
create_response = httpx.Response(httpx.codes.OK, json=new_resource_data)
15+
16+
with respx.mock:
17+
mock_route = respx.post("https://api.example.com/api/v1/test").mock(
18+
return_value=create_response
19+
)
20+
21+
created_resource = await async_dummy_service.create(resource_data)
22+
23+
assert created_resource.to_dict() == new_resource_data
24+
assert mock_route.call_count == 1
25+
request = mock_route.calls[0].request
26+
assert request.method == "POST"
27+
assert request.url == "https://api.example.com/api/v1/test"
28+
assert json.loads(request.content.decode()) == resource_data
29+
30+
31+
async def test_async_delete_mixin(async_dummy_service): # noqa: WPS210
32+
delete_response = httpx.Response(httpx.codes.NO_CONTENT, json=None)
33+
34+
with respx.mock:
35+
mock_route = respx.delete("https://api.example.com/api/v1/test/RES-123").mock(
36+
return_value=delete_response
37+
)
38+
39+
await async_dummy_service.delete("RES-123")
40+
41+
assert mock_route.call_count == 1
42+
43+
1144
async def test_async_fetch_one_success(async_dummy_service, single_result_response):
1245
with respx.mock:
1346
mock_route = respx.get("https://api.example.com/api/v1/test").mock(
@@ -244,39 +277,6 @@ async def test_async_iterate_lazy_evaluation(async_dummy_service):
244277
assert mock_route.call_count == 1
245278

246279

247-
async def test_async_create_resource(async_dummy_service): # noqa: WPS210
248-
resource_data = {"name": "Test Resource", "status": "active"}
249-
new_resource_data = {"id": "new-resource-id", "name": "Test Resource", "status": "active"}
250-
create_response = httpx.Response(httpx.codes.OK, json=new_resource_data)
251-
252-
with respx.mock:
253-
mock_route = respx.post("https://api.example.com/api/v1/test").mock(
254-
return_value=create_response
255-
)
256-
257-
created_resource = await async_dummy_service.create(resource_data)
258-
259-
assert created_resource.to_dict() == new_resource_data
260-
assert mock_route.call_count == 1
261-
request = mock_route.calls[0].request
262-
assert request.method == "POST"
263-
assert request.url == "https://api.example.com/api/v1/test"
264-
assert json.loads(request.content.decode()) == resource_data
265-
266-
267-
async def test_async_delete_resource(async_dummy_service): # noqa: WPS210
268-
delete_response = httpx.Response(httpx.codes.NO_CONTENT, json=None)
269-
270-
with respx.mock:
271-
mock_route = respx.delete("https://api.example.com/api/v1/test/RES-123").mock(
272-
return_value=delete_response
273-
)
274-
275-
await async_dummy_service.delete("RES-123")
276-
277-
assert mock_route.call_count == 1
278-
279-
280280
async def test_async_update_resource(async_dummy_service): # noqa: WPS210
281281
resource_data = {"name": "Test Resource", "status": "active"}
282282
update_response = httpx.Response(httpx.codes.OK, json=resource_data)

tests/http/test_service.py

Lines changed: 32 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,38 @@
99
from tests.http.conftest import DummyService
1010

1111

12+
def test_sync_create_mixin(dummy_service): # noqa: WPS210
13+
resource_data = {"name": "Test Resource", "status": "active"}
14+
new_resource_data = {"id": "new-resource-id", "name": "Test Resource", "status": "active"}
15+
create_response = httpx.Response(httpx.codes.OK, json=new_resource_data)
16+
17+
with respx.mock:
18+
mock_route = respx.post("https://api.example.com/api/v1/test").mock(
19+
return_value=create_response
20+
)
21+
22+
created_resource = dummy_service.create(resource_data)
23+
24+
assert created_resource.to_dict() == new_resource_data
25+
assert mock_route.call_count == 1
26+
request = mock_route.calls[0].request
27+
assert request.method == "POST"
28+
assert request.url == "https://api.example.com/api/v1/test"
29+
assert json.loads(request.content.decode()) == resource_data
30+
31+
32+
def test_sync_delete_mixin(dummy_service):
33+
delete_response = httpx.Response(httpx.codes.NO_CONTENT, json=None)
34+
with respx.mock:
35+
mock_route = respx.delete("https://api.example.com/api/v1/test/RES-123").mock(
36+
return_value=delete_response
37+
)
38+
39+
dummy_service.delete("RES-123")
40+
41+
assert mock_route.call_count == 1
42+
43+
1244
def test_sync_fetch_one_success(dummy_service, single_result_response):
1345
with respx.mock:
1446
mock_route = respx.get("https://api.example.com/api/v1/test").mock(
@@ -266,38 +298,6 @@ def test_sync_iterate_handles_api_errors(dummy_service):
266298
list(iterator)
267299

268300

269-
def test_sync_create_resource(dummy_service): # noqa: WPS210
270-
resource_data = {"name": "Test Resource", "status": "active"}
271-
new_resource_data = {"id": "new-resource-id", "name": "Test Resource", "status": "active"}
272-
create_response = httpx.Response(httpx.codes.OK, json=new_resource_data)
273-
274-
with respx.mock:
275-
mock_route = respx.post("https://api.example.com/api/v1/test").mock(
276-
return_value=create_response
277-
)
278-
279-
created_resource = dummy_service.create(resource_data)
280-
281-
assert created_resource.to_dict() == new_resource_data
282-
assert mock_route.call_count == 1
283-
request = mock_route.calls[0].request
284-
assert request.method == "POST"
285-
assert request.url == "https://api.example.com/api/v1/test"
286-
assert json.loads(request.content.decode()) == resource_data
287-
288-
289-
def test_sync_delete_resource(dummy_service):
290-
delete_response = httpx.Response(httpx.codes.NO_CONTENT, json=None)
291-
with respx.mock:
292-
mock_route = respx.delete("https://api.example.com/api/v1/test/RES-123").mock(
293-
return_value=delete_response
294-
)
295-
296-
dummy_service.delete("RES-123")
297-
298-
assert mock_route.call_count == 1
299-
300-
301301
def test_sync_update_resource(dummy_service):
302302
resource_data = {"name": "Test Resource", "status": "active"}
303303
update_response = httpx.Response(httpx.codes.OK, json=resource_data)

0 commit comments

Comments
 (0)