Skip to content

Commit 1186f54

Browse files
Reimplement bundling to make it less cumbersome when applied in multiple places (#480)
* Reimplement bundling to make it less cumbersome when applied in multiple places * Fix dataclass check
1 parent 9c9241b commit 1186f54

6 files changed

Lines changed: 225 additions & 139 deletions

File tree

‎encord/client.py‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1339,11 +1339,11 @@ def workflow_complete(self, label_hashes: List[str]) -> None:
13391339
payload=LabelWorkflowGraphNodePayload(action=WorkflowAction.COMPLETE),
13401340
)
13411341

1342-
def workflow_set_priority(self, params: TaskPriorityParams) -> None:
1342+
def workflow_set_priority(self, priorities: List[Tuple[str, float]]) -> None:
13431343
self._get_api_client().post(
13441344
Path(f"projects/{self.project_hash}/priorities"),
13451345
params=None,
1346-
payload=params,
1346+
payload=TaskPriorityParams(priorities=priorities),
13471347
result_type=None,
13481348
)
13491349

‎encord/http/bundle.py‎

Lines changed: 62 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,31 @@
1+
from __future__ import annotations
2+
3+
import dataclasses
14
import logging
2-
from dataclasses import asdict, dataclass
5+
from dataclasses import asdict, dataclass, is_dataclass
36
from 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

611
log = 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+
829
T = TypeVar("T")
930
R = 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

4968
class 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+
)

‎encord/http/limits.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
LABEL_ROW_BUNDLE_DEFAULT_LIMIT = 100
12
LABEL_ROW_BUNDLE_GET_LIMIT = 100
23
LABEL_ROW_BUNDLE_CREATE_LIMIT = 100
34
LABEL_ROW_BUNDLE_SAVE_LIMIT = 100
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
from __future__ import annotations
2+
3+
from dataclasses import dataclass
4+
from typing import Dict, List, Optional, Set, Tuple
5+
6+
"""
7+
Operation payloads to work with LabelRowV2 bundling.
8+
These are internal helpers and not supposed to be used by external users.
9+
"""
10+
11+
12+
@dataclass
13+
class BundledGetRowsPayload:
14+
uids: List[str]
15+
get_signed_url: bool
16+
include_object_feature_hashes: Optional[Set[str]]
17+
include_classification_feature_hashes: Optional[Set[str]]
18+
include_reviews: bool
19+
20+
def add(self, other: BundledGetRowsPayload) -> BundledGetRowsPayload:
21+
self.uids.extend(other.uids)
22+
return self
23+
24+
25+
@dataclass
26+
class BundledCreateRowsPayload:
27+
uids: List[str]
28+
29+
def add(self, other: BundledCreateRowsPayload) -> BundledCreateRowsPayload:
30+
self.uids.extend(other.uids)
31+
return self
32+
33+
34+
@dataclass
35+
class BundledSaveRowsPayload:
36+
uids: List[str]
37+
payload: List[Dict]
38+
39+
def add(self, other: BundledSaveRowsPayload) -> BundledSaveRowsPayload:
40+
self.uids.extend(other.uids)
41+
self.payload.extend(other.payload)
42+
return self
43+
44+
45+
@dataclass
46+
class BundledSetPriorityPayload:
47+
priorities: List[Tuple[str, float]]
48+
49+
def add(self, other: BundledSetPriorityPayload) -> BundledSetPriorityPayload:
50+
self.priorities.extend(other.priorities)
51+
return self
52+
53+
54+
@dataclass
55+
class BundledWorkflowCompletePayload:
56+
label_hashes: List[str]
57+
58+
def add(self, other: BundledWorkflowCompletePayload) -> BundledWorkflowCompletePayload:
59+
self.label_hashes.extend(other.label_hashes)
60+
return self
61+
62+
63+
@dataclass
64+
class BundledWorkflowReopenPayload:
65+
label_hashes: List[str]
66+
67+
def add(self, other: BundledWorkflowReopenPayload) -> BundledWorkflowReopenPayload:
68+
self.label_hashes.extend(other.label_hashes)
69+
return self

0 commit comments

Comments
 (0)