2828_max_sleep = 15
2929
3030UT = t .TypeVar ("UT" , str , int , float , datetime .datetime , bool , None )
31+ NT = t .TypeVar ("NT" , str , int , float , datetime .datetime , bool , oa_types .Unset )
3132
3233
33- class BaseClient :
34+ class BaseClient ( t . Generic [ T ]) :
3435 """The overall base client for all project related clients."""
3536
36- _retry_methods : t .ClassVar [list [str ]] = []
37+ _retry_methods : t .ClassVar [set [str ]] = set ()
3738
3839 def __init__ (
3940 self , project_id : str , client : "polarion_client.PolarionClient"
4041 ):
4142 self ._project_id = project_id
4243 self ._client = client
44+ self ._all_retry_methods : set [str ] = set ()
45+ for base in self .__class__ .__mro__ :
46+ if hasattr (base , "_retry_methods" ):
47+ self ._all_retry_methods .update (base ._retry_methods )
4348
4449 def __getattribute__ (self , name : str ) -> t .Any :
4550 """Retry method calls defined in _retry_methods."""
4651 attr = super ().__getattribute__ (name )
47- retry_methods = super ().__getattribute__ ("_retry_methods " )
52+ retry_methods = super ().__getattribute__ ("_all_retry_methods " )
4853 if name in retry_methods and callable (attr ):
4954 return functools .partial (
5055 super ().__getattribute__ ("_retry_on_error" ), attr
@@ -100,6 +105,20 @@ def unset_to_none(self, value: t.Any) -> t.Any:
100105 return None
101106 return value
102107
108+ @t .overload
109+ def none_to_unset (self , value : None ) -> oa_types .Unset :
110+ """Return UNSET if value is None, else the value."""
111+
112+ @t .overload
113+ def none_to_unset (self , value : NT ) -> NT :
114+ """Return UNSET if value is None, else the value."""
115+
116+ def none_to_unset (self , value : t .Any ) -> t .Any :
117+ """Return UNSET if value is None, else the value."""
118+ if value is None :
119+ return oa_types .UNSET
120+ return value
121+
103122 def _raise_on_error (self , response : oa_types .Response ) -> None :
104123 def unexpected_error () -> errors .PolarionApiUnexpectedException :
105124 return errors .PolarionApiUnexpectedException (
@@ -152,16 +171,112 @@ def _retry_on_error(
152171 time .sleep (random .uniform (_min_sleep , _max_sleep ))
153172 return call (* args , ** kwargs )
154173
174+ def _pre_batching_grouping (
175+ self , items : list [T ]
176+ ) -> t .Generator [list [T ], None , None ]:
177+ yield items
155178
156- class ItemsClient (BaseClient , t .Generic [T ], abc .ABC ):
157- """A client for items of a project, which can be created or requested."""
158179
159- _retry_methods : t .ClassVar [list [str ]] = [
160- "get_multi" ,
180+ class SingleGetClient (BaseClient [T ], abc .ABC ):
181+ """A client for items of a project, which can be created."""
182+
183+ _retry_methods : t .ClassVar [set [str ]] = {
161184 "get" ,
185+ "a_get" ,
186+ }
187+
188+ @abc .abstractmethod
189+ def get (self , * args : t .Any , ** kwargs : t .Any ) -> T | None :
190+ """Get a specific single item."""
191+
192+ @abc .abstractmethod
193+ async def a_get (self , * args : t .Any , ** kwargs : t .Any ) -> T | None :
194+ """Get a specific single item."""
195+
196+
197+ class CreateClient (BaseClient [T ], abc .ABC ):
198+ """A client for items of a project, which can be created."""
199+
200+ _retry_methods : t .ClassVar [set [str ]] = {
162201 "_create" ,
202+ "_a_create" ,
203+ }
204+ _create_batch_size : int = 0
205+
206+ @abc .abstractmethod
207+ def _create (self , items : list [T ]) -> None : ...
208+
209+ @abc .abstractmethod
210+ async def _a_create (self , items : list [T ]) -> None : ...
211+
212+ def _split_into_create_batches (
213+ self , items : list [T ]
214+ ) -> t .Generator [list [T ], None , None ]:
215+ batch_size = self ._create_batch_size or self ._client .batch_size
216+ for it in self ._pre_batching_grouping (items ):
217+ for i in range (0 , len (it ), batch_size ):
218+ yield it [i : i + batch_size ]
219+
220+ def create (self , items : T | list [T ]) -> None :
221+ """Create one or multiple items."""
222+ if not isinstance (items , list ):
223+ items = [items ]
224+
225+ for batch in self ._split_into_create_batches (items ):
226+ self ._create (batch )
227+
228+ async def a_create (self , items : T | list [T ]) -> None :
229+ """Create one or multiple items."""
230+ if not isinstance (items , list ):
231+ items = [items ]
232+
233+ for batch in self ._split_into_create_batches (items ):
234+ await self ._a_create (batch )
235+
236+
237+ class DeleteClient (BaseClient [T ], abc .ABC ):
238+ """A client for items of a project, which can be created."""
239+
240+ _retry_methods : t .ClassVar [set [str ]] = {
163241 "_delete" ,
164- ]
242+ }
243+ _delete_batch_size : int = 0
244+
245+ def _split_into_delete_batches (
246+ self , items : list [T ]
247+ ) -> t .Generator [list [T ], None , None ]:
248+ batch_size = self ._delete_batch_size or self ._client .batch_size
249+ for it in self ._pre_batching_grouping (items ):
250+ for i in range (0 , len (it ), batch_size ):
251+ yield it [i : i + batch_size ]
252+
253+ @abc .abstractmethod
254+ def _delete (self , items : list [T ]) -> None : ...
255+
256+ @abc .abstractmethod
257+ async def _a_delete (self , items : list [T ]) -> None : ...
258+
259+ def delete (self , items : T | list [T ]) -> None :
260+ """Delete one or multiple items."""
261+ if not isinstance (items , list ):
262+ items = [items ]
263+ for batch in self ._split_into_delete_batches (items ):
264+ self ._delete (batch )
265+
266+ async def a_delete (self , items : T | list [T ]) -> None :
267+ """Delete one or multiple items."""
268+ if not isinstance (items , list ):
269+ items = [items ]
270+ for batch in self ._split_into_delete_batches (items ):
271+ await self ._a_delete (batch )
272+
273+
274+ class MultiGetClient (BaseClient [T ], abc .ABC ):
275+ """A client for items of a project, which can be created."""
276+
277+ _retry_methods : t .ClassVar [set [str ]] = {
278+ "get_multi" ,
279+ }
165280
166281 @abc .abstractmethod
167282 def get_multi (
@@ -178,9 +293,18 @@ def get_multi(
178293 """
179294
180295 @abc .abstractmethod
181- def get (self , * args : t .Any , ** kwargs : t .Any ) -> T | None :
182- """Get a specific single item."""
183- return self ._retry_on_error (self .get , * args , ** kwargs )
296+ async def a_get_multi (
297+ self ,
298+ * args : t .Any ,
299+ page_size : int = 100 ,
300+ page_number : int = 1 ,
301+ ** kwargs : t .Any ,
302+ ) -> tuple [list [T ], bool ]:
303+ """Get multiple matching items for a specific page.
304+
305+ In addition, a flag whether a next page is available is
306+ returned.
307+ """
184308
185309 def get_all (self , * args : t .Any , ** kwargs : t .Any ) -> list [T ]:
186310 """Return all matching items using get_multi with auto pagination."""
@@ -199,52 +323,45 @@ def get_all(self, *args: t.Any, **kwargs: t.Any) -> list[T]:
199323 items += _items
200324 return items
201325
202- @abc .abstractmethod
203- def _create (self , items : list [T ]) -> None : ...
204-
205- def _split_into_batches (
206- self , items : list [T ]
207- ) -> t .Generator [list [T ], None , None ]:
208- for i in range (0 , len (items ), self ._client .batch_size ):
209- yield items [i : i + self ._client .batch_size ]
210-
211- def create (self , items : T | list [T ]) -> None :
212- """Create one or multiple items."""
213- if not isinstance (items , list ):
214- items = [items ]
215-
216- for batch in self ._split_into_batches (items ):
217- self ._create (batch )
218-
219- @abc .abstractmethod
220- def _delete (self , items : list [T ]) -> None : ...
221-
222- def delete (self , items : T | list [T ]) -> None :
223- """Delete one or multiple items."""
224- if not isinstance (items , list ):
225- items = [items ]
226- for batch in self ._split_into_batches (items ):
227- self ._delete (batch )
326+ async def a_get_all (self , * args : t .Any , ** kwargs : t .Any ) -> list [T ]:
327+ """Return all matching items using get_multi with auto pagination."""
328+ page = 1
329+ items , next_page = await self .a_get_multi (
330+ * args , page_size = self ._client .page_size , page_number = page , ** kwargs
331+ )
332+ while next_page :
333+ page += 1
334+ _items , next_page = await self .a_get_multi (
335+ * args ,
336+ page_size = self ._client .page_size ,
337+ page_number = page ,
338+ ** kwargs ,
339+ )
340+ items += _items
341+ return items
228342
229343
230- class UpdatableItemsClient ( ItemsClient , t . Generic [T ], abc .ABC ):
344+ class UpdateClient ( BaseClient [T ], abc .ABC ):
231345 """A client for items which can also be updated."""
232346
233- _retry_methods : t .ClassVar [list [str ]] = [
234- "get_multi" ,
235- "get" ,
236- "_create" ,
237- "_delete" ,
347+ _retry_methods : t .ClassVar [set [str ]] = {
238348 "_update" ,
239- ]
349+ }
350+ _update_batch_size : int = 0
240351
241352 def _split_into_update_batches (
242353 self , items : list [T ]
243- ) -> t .Generator [list [T ], None , None ] | t .Generator [T , None , None ]:
244- yield from self ._split_into_batches (items )
354+ ) -> t .Generator [list [T ], None , None ]:
355+ batch_size = self ._update_batch_size or self ._client .batch_size
356+ for it in self ._pre_batching_grouping (items ):
357+ for i in range (0 , len (it ), batch_size ):
358+ yield it [i : i + batch_size ]
245359
246360 @abc .abstractmethod
247- def _update (self , to_update : T | list [T ]) -> None : ...
361+ def _update (self , to_update : list [T ]) -> None : ...
362+
363+ @abc .abstractmethod
364+ async def _a_update (self , to_update : list [T ]) -> None : ...
248365
249366 def update (self , items : T | list [T ]) -> None :
250367 """Update the provided item or items."""
@@ -254,17 +371,16 @@ def update(self, items: T | list[T]) -> None:
254371 for batch in self ._split_into_update_batches (items ):
255372 self ._update (batch )
256373
374+ async def a_update (self , items : T | list [T ]) -> None :
375+ """Update the provided item or items."""
376+ if not isinstance (items , list ):
377+ items = [items ]
257378
258- class SingleUpdatableItemsMixin (t .Generic [T ]):
259- """Mixin to split batches into single items."""
260-
261- def _split_into_update_batches (
262- self , items : list [T ]
263- ) -> t .Generator [T , None , None ]:
264- yield from items
379+ for batch in self ._split_into_update_batches (items ):
380+ await self ._a_update (batch )
265381
266382
267- class StatusItemClient (UpdatableItemsClient , t .Generic [ST ], abc .ABC ):
383+ class StatusItemClient (UpdateClient , DeleteClient , t .Generic [ST ], abc .ABC ):
268384 """A client for items, which have a status.
269385
270386 We support to set a specific status for these instead of deleting
@@ -287,12 +403,24 @@ def delete(self, items: ST | list[ST]) -> None:
287403 if self .delete_status is None :
288404 super ().delete (items )
289405 else :
290- if not isinstance (items , list ):
291- items = [items ]
292- delete_items : list [ST ] = []
293- for item in items :
294- item .status = self .delete_status
295- delete_items .append (
296- self .item_cls (id = item .id , status = self .delete_status )
297- )
406+ delete_items = self ._prepare_update_items (items )
298407 self .update (delete_items )
408+
409+ async def a_delete (self , items : ST | list [ST ]) -> None :
410+ """Delete the item if no delete_status was set, else update status."""
411+ if self .delete_status is None :
412+ await super ().a_delete (items )
413+ else :
414+ delete_items = self ._prepare_update_items (items )
415+ await self .a_update (delete_items )
416+
417+ def _prepare_update_items (self , items : ST | list [ST ]) -> list [ST ]:
418+ if not isinstance (items , list ):
419+ items = [items ]
420+ delete_items : list [ST ] = []
421+ for item in items :
422+ item .status = self .delete_status
423+ delete_items .append (
424+ self .item_cls (id = item .id , status = self .delete_status )
425+ )
426+ return delete_items
0 commit comments