11import copy
22from abc import ABC
3- from collections .abc import Iterator
3+ from collections .abc import AsyncIterator , Iterator
44from typing import Any , Self
55
66import httpx
77
8- from mpt_api_client .http .client import HTTPClient
8+ from mpt_api_client .http .client import HTTPClient , HTTPClientAsync
99from mpt_api_client .http .resource import ResourceBaseClient
1010from mpt_api_client .models import Collection , Resource
1111from mpt_api_client .rql .query_builder import RQLQuery
1212
1313
14- class CollectionBaseClient [ResourceModel : Resource , ResourceClient : ResourceBaseClient [Resource ]]( # noqa: WPS214
15- ABC
16- ):
17- """Immutable Base client for RESTful resource collections.
18-
19- Examples:
20- active_orders_cc = order_collection.filter(RQLQuery(status="active"))
21- active_orders = active_orders_cc.order_by("created").iterate()
22- product_active_orders = active_orders_cc.filter(RQLQuery(product__id="PRD-1")).iterate()
23-
24- new_order = order_collection.create(order_data)
25-
26- """
14+ class CollectionMixin :
15+ """Mixin for collection clients."""
2716
2817 _endpoint : str
29- _resource_class : type [ResourceModel ]
30- _resource_client_class : type [ResourceClient ]
31- _collection_class : type [Collection [ResourceModel ]]
18+ _resource_class : type [Any ]
19+ _resource_client_class : type [Any ]
20+ _collection_class : type [Collection [Any ]]
3221
3322 def __init__ (
3423 self ,
24+ http_client : HTTPClient | HTTPClientAsync ,
3525 query_rql : RQLQuery | None = None ,
36- client : HTTPClient | None = None ,
3726 ) -> None :
38- self .mpt_client = client or HTTPClient ()
27+ self .http_client = http_client
3928 self .query_rql : RQLQuery | None = query_rql
4029 self .query_order_by : list [str ] | None = None
4130 self .query_select : list [str ] | None = None
4231
4332 @classmethod
44- def clone (
45- cls , collection_client : "CollectionBaseClient[ResourceModel, ResourceClient]"
46- ) -> Self :
33+ def clone (cls , collection_client : "CollectionMixin" ) -> Self :
4734 """Create a copy of collection client for immutable operations.
4835
4936 Returns:
5037 New collection client with same settings.
5138 """
5239 new_collection = cls (
53- client = collection_client .mpt_client ,
40+ http_client = collection_client .http_client ,
5441 query_rql = collection_client .query_rql ,
5542 )
5643 new_collection .query_order_by = (
@@ -128,6 +115,33 @@ def select(self, *fields: str) -> Self:
128115 new_client .query_select = list (fields )
129116 return new_client
130117
118+
119+ class CollectionClientBase [ResourceModel : Resource , ResourceClient : ResourceBaseClient [Resource ]]( # noqa: WPS214
120+ ABC , CollectionMixin
121+ ):
122+ """Immutable Base client for RESTful resource collections.
123+
124+ Examples:
125+ active_orders_cc = order_collection.filter(RQLQuery(status="active"))
126+ active_orders = active_orders_cc.order_by("created").iterate()
127+ product_active_orders = active_orders_cc.filter(RQLQuery(product__id="PRD-1")).iterate()
128+
129+ new_order = order_collection.create(order_data)
130+
131+ """
132+
133+ _resource_class : type [ResourceModel ]
134+ _resource_client_class : type [ResourceClient ]
135+ _collection_class : type [Collection [ResourceModel ]]
136+
137+ def __init__ (
138+ self ,
139+ query_rql : RQLQuery | None = None ,
140+ http_client : HTTPClient | None = None ,
141+ ) -> None :
142+ self .http_client : HTTPClient = http_client or HTTPClient () # type: ignore[mutable-override]
143+ CollectionMixin .__init__ (self , http_client = self .http_client , query_rql = query_rql )
144+
131145 def fetch_page (self , limit : int = 100 , offset : int = 0 ) -> Collection [ResourceModel ]:
132146 """Fetch one page of resources.
133147
@@ -185,15 +199,15 @@ def iterate(self, batch_size: int = 100) -> Iterator[ResourceModel]:
185199
186200 def get (self , resource_id : str ) -> ResourceClient :
187201 """Get resource by resource_id."""
188- return self ._resource_client_class (client = self .mpt_client , resource_id = resource_id )
202+ return self ._resource_client_class (http_client = self .http_client , resource_id = resource_id )
189203
190204 def create (self , resource_data : dict [str , Any ]) -> ResourceModel :
191205 """Create a new resource using `POST /endpoint`.
192206
193207 Returns:
194208 New resource created.
195209 """
196- response = self .mpt_client .post (self ._endpoint , json = resource_data )
210+ response = self .http_client .post (self ._endpoint , json = resource_data )
197211 response .raise_for_status ()
198212
199213 return self ._resource_class .from_response (response )
@@ -208,7 +222,121 @@ def _fetch_page_as_response(self, limit: int = 100, offset: int = 0) -> httpx.Re
208222 HTTPStatusError: if the response status code is not 200.
209223 """
210224 pagination_params : dict [str , int ] = {"limit" : limit , "offset" : offset }
211- response = self .mpt_client .get (self .build_url (pagination_params ))
225+ response = self .http_client .get (self .build_url (pagination_params ))
226+ response .raise_for_status ()
227+
228+ return response
229+
230+
231+ class AsyncCollectionClientBase [
232+ ResourceModel : Resource ,
233+ ResourceClient : ResourceBaseClient [Resource ],
234+ ](ABC , CollectionMixin ):
235+ """Immutable Base client for RESTful resource collections.
236+
237+ Examples:
238+ active_orders_cc = order_collection.filter(RQLQuery(status="active"))
239+ active_orders = active_orders_cc.order_by("created").iterate()
240+ product_active_orders = active_orders_cc.filter(RQLQuery(product__id="PRD-1")).iterate()
241+
242+ new_order = order_collection.create(order_data)
243+
244+ """
245+
246+ _resource_class : type [ResourceModel ]
247+ _resource_client_class : type [ResourceClient ]
248+ _collection_class : type [Collection [ResourceModel ]]
249+
250+ def __init__ (
251+ self ,
252+ query_rql : RQLQuery | None = None ,
253+ http_client : HTTPClientAsync | None = None ,
254+ ) -> None :
255+ self .http_client : HTTPClientAsync = http_client or HTTPClientAsync () # type: ignore[mutable-override]
256+ CollectionMixin .__init__ (self , http_client = self .http_client , query_rql = query_rql )
257+
258+ async def fetch_page (self , limit : int = 100 , offset : int = 0 ) -> Collection [ResourceModel ]:
259+ """Fetch one page of resources.
260+
261+ Returns:
262+ Collection of resources.
263+ """
264+ response = await self ._fetch_page_as_response (limit = limit , offset = offset )
265+ return Collection .from_response (response )
266+
267+ async def fetch_one (self ) -> ResourceModel :
268+ """Fetch one page, expect exactly one result.
269+
270+ Returns:
271+ One resource.
272+
273+ Raises:
274+ ValueError: If the total matching records are not exactly one.
275+ """
276+ response = await self ._fetch_page_as_response (limit = 1 , offset = 0 )
277+ resource_list : Collection [ResourceModel ] = Collection .from_response (response )
278+ total_records = len (resource_list )
279+ if resource_list .meta :
280+ total_records = resource_list .meta .pagination .total
281+ if total_records == 0 :
282+ raise ValueError ("Expected one result, but got zero results" )
283+ if total_records > 1 :
284+ raise ValueError (f"Expected one result, but got { total_records } results" )
285+
286+ return resource_list [0 ]
287+
288+ async def iterate (self , batch_size : int = 100 ) -> AsyncIterator [ResourceModel ]:
289+ """Iterate over all resources, yielding GenericResource objects.
290+
291+ Args:
292+ batch_size: Number of resources to fetch per request
293+
294+ Returns:
295+ Iterator of resources.
296+ """
297+ offset = 0
298+ limit = batch_size # Default page size
299+
300+ while True :
301+ response = await self ._fetch_page_as_response (limit = limit , offset = offset )
302+ items_collection : Collection [ResourceModel ] = self ._collection_class .from_response (
303+ response
304+ )
305+ for resource in items_collection :
306+ yield resource
307+
308+ if not items_collection .meta :
309+ break
310+ if not items_collection .meta .pagination .has_next ():
311+ break
312+ offset = items_collection .meta .pagination .next_offset ()
313+
314+ async def get (self , resource_id : str ) -> ResourceClient :
315+ """Get resource by resource_id."""
316+ return self ._resource_client_class (http_client = self .http_client , resource_id = resource_id ) # type: ignore[arg-type]
317+
318+ async def create (self , resource_data : dict [str , Any ]) -> ResourceModel :
319+ """Create a new resource using `POST /endpoint`.
320+
321+ Returns:
322+ New resource created.
323+ """
324+ response = await self .http_client .post (self ._endpoint , json = resource_data )
325+ response .raise_for_status ()
326+
327+ return self ._resource_class .from_response (response )
328+
329+ async def _fetch_page_as_response (self , limit : int = 100 , offset : int = 0 ) -> httpx .Response :
330+ """Fetch one page of resources.
331+
332+ Returns:
333+ httpx.Response object.
334+
335+ Raises:
336+ HTTPStatusError: if the response status code is not 200.
337+ """
338+ pagination_params : dict [str , int ] = {"limit" : limit , "offset" : offset }
339+ response = await self .http_client .get (self .build_url (pagination_params ))
212340 response .raise_for_status ()
213341
214342 return response
0 commit comments