1+ from __future__ import annotations
2+
3+ import dataclasses
14import logging
2- from dataclasses import asdict , dataclass
5+ from dataclasses import asdict , dataclass , is_dataclass
36from functools import reduce
4- from typing import Callable , Dict , Generator , Generic , List , Optional , TypeVar
7+ from typing import Callable , ClassVar , Dict , Generator , Generic , List , Optional , Protocol , Type , TypeVar
8+
9+ from encord .http .limits import LABEL_ROW_BUNDLE_DEFAULT_LIMIT
510
611log = logging .getLogger (__name__ )
712
13+ BundlablePayloadT = TypeVar ("BundlablePayloadT" , bound = "BundlablePayload" )
14+
15+
16+ class BundlablePayload (Protocol [BundlablePayloadT ]):
17+ """
18+ All payloads that work with bundles need to provide "add" method
19+ that would allow bundler to combine multiple payloads into one or few aggregates.
20+ """
21+
22+ # This line ensures we're only allowing dataclasses for now
23+ __dataclass_fields__ : ClassVar [Dict ]
24+
25+ def add (self , other : BundlablePayloadT ) -> BundlablePayloadT :
26+ ...
27+
28+
829T = TypeVar ("T" )
930R = TypeVar ("R" )
1031
@@ -21,29 +42,27 @@ class BundleResultMapper(Generic[T]):
2142 result_handler : BundleResultHandler [T ]
2243
2344
24- class BundledOperation (Generic [T , R ]):
45+ class BundledOperation (Generic [BundlablePayloadT , R ]):
2546 def __init__ (
2647 self ,
2748 operation : Callable [..., List [R ]],
28- request_reducer : Callable [[T , T ], T ],
2949 result_mapper : Optional [Callable [[R ], str ]],
3050 limit : int ,
3151 ) -> None :
3252 self .operation = operation
33- self .request_reducer = request_reducer
3453 self .result_mapper = result_mapper
3554 self .limit = limit
36- self .payloads : List [T ] = []
37- self .result_handlers : Dict [str , Callable [[T ], None ]] = dict ()
55+ self .payloads : List [BundlablePayloadT ] = []
56+ self .result_handlers : Dict [str , Callable [[BundlablePayloadT ], None ]] = {}
3857
39- def append (self , payload : T , result_handler : Optional [BundleResultHandler ]):
58+ def append (self , payload : BundlablePayloadT , result_handler : Optional [BundleResultHandler ]):
4059 self .payloads .append (payload )
4160 if result_handler is not None :
4261 self .result_handlers [result_handler .predicate ] = result_handler .handler
4362
44- def get_bundled_payload (self ) -> Generator [T , None , None ]:
63+ def get_bundled_payload (self ) -> Generator [BundlablePayloadT , None , None ]:
4564 for i in range (0 , len (self .payloads ), self .limit ):
46- yield reduce (self . request_reducer , self .payloads [i : i + self .limit ])
65+ yield reduce (lambda x , y : x . add ( y ) , self .payloads [i : i + self .limit ])
4766
4867
4968class Bundle :
@@ -87,36 +106,37 @@ class Bundle:
87106 """
88107
89108 def __init__ (self ) -> None :
90- self ._operations : Dict [Callable , BundledOperation ] = dict ()
109+ self ._operations : Dict [Callable , BundledOperation ] = {}
91110
92111 def __register_operation (
93112 self ,
113+ payload_type : Type [BundlablePayloadT ],
94114 operation : Callable [..., List [R ]],
95- request_reducer : Callable [[T , T ], T ],
96115 result_mapping_predicate : Optional [Callable [[R ], str ]],
97116 limit : int ,
98- ) -> BundledOperation [T , R ]:
117+ ) -> BundledOperation [BundlablePayloadT , R ]:
99118 if operation not in self ._operations :
100- self ._operations [operation ] = BundledOperation (operation , request_reducer , result_mapping_predicate , limit )
119+ self ._operations [operation ] = BundledOperation [BundlablePayloadT , R ](
120+ operation , result_mapping_predicate , limit
121+ )
101122
102123 return self ._operations [operation ]
103124
104125 def add (
105126 self ,
106127 operation : Callable [..., List [R ]],
107- request_reducer : Callable [[T , T ], T ],
108128 result_mapper : Optional [BundleResultMapper [R ]],
109- payload : T ,
129+ payload : BundlablePayloadT ,
110130 limit : int ,
111131 ) -> None :
112132 """
113133 This is an internal method and normally is not supposed to be used externally.
114134
115135 Adds an operation to a bundle for delayed execution.
116136 """
117- result_mapping_predicate = result_mapper .result_mapping_predicate if result_mapper is not None else None
118- result_handler = result_mapper .result_handler if result_mapper is not None else None
119- self .__register_operation (operation , request_reducer , result_mapping_predicate , limit ).append (
137+ result_mapping_getter = result_mapper .result_mapping_predicate if result_mapper else None
138+ result_handler = result_mapper .result_handler if result_mapper else None
139+ self .__register_operation (type ( payload ), operation , result_mapping_getter , limit ).append (
120140 payload , result_handler
121141 )
122142
@@ -143,3 +163,26 @@ def __exit__(self, exc_type, exc_value, exc_traceback):
143163 log .warning (f"Cancelling operation due to exception: { exc_type .__name__ } " )
144164 else :
145165 self .execute ()
166+
167+
168+ def bundled_operation (
169+ bundle ,
170+ operation ,
171+ payload : BundlablePayloadT ,
172+ result_mapper : Optional [BundleResultMapper ] = None ,
173+ limit : int = LABEL_ROW_BUNDLE_DEFAULT_LIMIT ,
174+ ) -> None :
175+ assert is_dataclass (payload ), "Bundling only works with dataclasses"
176+ if not bundle :
177+ result = operation (** asdict (payload ))
178+ if result_mapper :
179+ assert len (result ) == 1 , "Expected a singular request for singular request!"
180+ assert result_mapper .result_mapping_predicate (result [0 ]) == result_mapper .result_handler .predicate
181+ result_mapper .result_handler .handler (result [0 ])
182+ else :
183+ bundle .add (
184+ operation = operation ,
185+ result_mapper = result_mapper ,
186+ payload = payload ,
187+ limit = limit ,
188+ )
0 commit comments