aio-sf 0.1.0b9__tar.gz → 0.1.0b11__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/PKG-INFO +11 -7
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/README.md +10 -6
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/collections/__init__.py +3 -3
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/collections/batch.py +53 -38
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/collections/client.py +13 -13
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/tests/test_retry_and_batch.py +45 -35
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/.cursor/rules/api-structure.mdc +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/.cursor/rules/async-patterns.mdc +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/.cursor/rules/project-tooling.mdc +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/.github/workflows/publish.yml +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/.github/workflows/test.yml +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/.gitignore +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/LICENSE +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/RELEASE.md +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/pyproject.toml +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/pytest.ini +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/auth/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/auth/base.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/auth/client_credentials.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/auth/refresh_token.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/auth/sfdx_cli.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/auth/static_token.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/bulk_v2/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/bulk_v2/client.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/bulk_v2/types.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/client.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/collections/records.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/collections/retry.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/collections/types.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/describe/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/describe/client.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/describe/types.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/query/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/query/client.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/query/types.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/api/types.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/exporter/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/exporter/bulk_export.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/src/aio_sf/exporter/parquet_writer.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/tests/__init__.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/tests/conftest.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/tests/test_api_clients.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/tests/test_auth.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/tests/test_client.py +0 -0
- {aio_sf-0.1.0b9 → aio_sf-0.1.0b11}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aio-sf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0b11
|
|
4
4
|
Summary: Async Salesforce library for Python
|
|
5
5
|
Project-URL: Homepage, https://github.com/callawaycloud/aio-salesforce
|
|
6
6
|
Project-URL: Repository, https://github.com/callawaycloud/aio-salesforce
|
|
@@ -166,15 +166,19 @@ async with SalesforceClient(auth_strategy=auth) as sf:
|
|
|
166
166
|
|
|
167
167
|
**Advanced - With Retries, Concurrency Scaling, and Progress:**
|
|
168
168
|
```python
|
|
169
|
-
from aio_sf.api.collections import
|
|
169
|
+
from aio_sf.api.collections import ResultInfo
|
|
170
170
|
|
|
171
|
-
async def
|
|
171
|
+
async def on_result(info: ResultInfo):
|
|
172
|
+
# Called after each batch completes with successes and errors split
|
|
172
173
|
print(
|
|
173
|
-
f"
|
|
174
|
-
f"{info['
|
|
175
|
-
f"{info['records_failed']} failed, "
|
|
174
|
+
f"Batch: {len(info['successes'])} succeeded, {len(info['errors'])} failed | "
|
|
175
|
+
f"Attempt {info['current_attempt']}, "
|
|
176
|
+
f"Overall: {info['records_succeeded']} OK, {info['records_failed']} failed, "
|
|
176
177
|
f"{info['records_pending']} pending"
|
|
177
178
|
)
|
|
179
|
+
# Inspect errors (includes both API errors and HTTP failures)
|
|
180
|
+
for error in info['errors']:
|
|
181
|
+
print(f" Error: {error['errors']}")
|
|
178
182
|
|
|
179
183
|
async with SalesforceClient(auth_strategy=auth) as sf:
|
|
180
184
|
results = await sf.collections.insert(
|
|
@@ -183,7 +187,7 @@ async with SalesforceClient(auth_strategy=auth) as sf:
|
|
|
183
187
|
batch_size=[200, 100, 25], # Shrink batch size on retry
|
|
184
188
|
max_concurrent_batches=[5, 3, 1], # Reduce concurrency on retry
|
|
185
189
|
max_attempts=5, # Retry up to 5 times
|
|
186
|
-
|
|
190
|
+
on_result=on_result, # Callback with results
|
|
187
191
|
)
|
|
188
192
|
```
|
|
189
193
|
|
|
@@ -103,15 +103,19 @@ async with SalesforceClient(auth_strategy=auth) as sf:
|
|
|
103
103
|
|
|
104
104
|
**Advanced - With Retries, Concurrency Scaling, and Progress:**
|
|
105
105
|
```python
|
|
106
|
-
from aio_sf.api.collections import
|
|
106
|
+
from aio_sf.api.collections import ResultInfo
|
|
107
107
|
|
|
108
|
-
async def
|
|
108
|
+
async def on_result(info: ResultInfo):
|
|
109
|
+
# Called after each batch completes with successes and errors split
|
|
109
110
|
print(
|
|
110
|
-
f"
|
|
111
|
-
f"{info['
|
|
112
|
-
f"{info['records_failed']} failed, "
|
|
111
|
+
f"Batch: {len(info['successes'])} succeeded, {len(info['errors'])} failed | "
|
|
112
|
+
f"Attempt {info['current_attempt']}, "
|
|
113
|
+
f"Overall: {info['records_succeeded']} OK, {info['records_failed']} failed, "
|
|
113
114
|
f"{info['records_pending']} pending"
|
|
114
115
|
)
|
|
116
|
+
# Inspect errors (includes both API errors and HTTP failures)
|
|
117
|
+
for error in info['errors']:
|
|
118
|
+
print(f" Error: {error['errors']}")
|
|
115
119
|
|
|
116
120
|
async with SalesforceClient(auth_strategy=auth) as sf:
|
|
117
121
|
results = await sf.collections.insert(
|
|
@@ -120,7 +124,7 @@ async with SalesforceClient(auth_strategy=auth) as sf:
|
|
|
120
124
|
batch_size=[200, 100, 25], # Shrink batch size on retry
|
|
121
125
|
max_concurrent_batches=[5, 3, 1], # Reduce concurrency on retry
|
|
122
126
|
max_attempts=5, # Retry up to 5 times
|
|
123
|
-
|
|
127
|
+
on_result=on_result, # Callback with results
|
|
124
128
|
)
|
|
125
129
|
```
|
|
126
130
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Salesforce Collections API module."""
|
|
2
2
|
|
|
3
3
|
from .client import CollectionsAPI
|
|
4
|
-
from .batch import
|
|
4
|
+
from .batch import ResultInfo, ResultCallback
|
|
5
5
|
from .retry import ShouldRetryCallback, default_should_retry
|
|
6
6
|
from .types import (
|
|
7
7
|
CollectionError,
|
|
@@ -14,8 +14,8 @@ from .types import (
|
|
|
14
14
|
|
|
15
15
|
__all__ = [
|
|
16
16
|
"CollectionsAPI",
|
|
17
|
-
"
|
|
18
|
-
"
|
|
17
|
+
"ResultInfo",
|
|
18
|
+
"ResultCallback",
|
|
19
19
|
"ShouldRetryCallback",
|
|
20
20
|
"default_should_retry",
|
|
21
21
|
"CollectionError",
|
|
@@ -17,21 +17,25 @@ from .types import CollectionResult
|
|
|
17
17
|
logger = logging.getLogger(__name__)
|
|
18
18
|
|
|
19
19
|
|
|
20
|
-
class
|
|
21
|
-
"""
|
|
20
|
+
class ResultInfo(TypedDict):
|
|
21
|
+
"""Result information provided after each batch completes."""
|
|
22
22
|
|
|
23
|
+
successes: List[CollectionResult] # Successful results from this batch
|
|
24
|
+
errors: List[
|
|
25
|
+
CollectionResult
|
|
26
|
+
] # Failed results from this batch (API and HTTP errors)
|
|
23
27
|
total_records: int # Total records being processed
|
|
24
28
|
records_completed: int # Records finished (succeeded or failed permanently)
|
|
25
|
-
records_succeeded: int # Records that succeeded
|
|
26
|
-
records_failed: int # Records that failed permanently
|
|
29
|
+
records_succeeded: int # Records that succeeded so far
|
|
30
|
+
records_failed: int # Records that failed permanently so far
|
|
27
31
|
records_pending: int # Records still being retried
|
|
28
32
|
current_attempt: int # Current retry attempt number (1-indexed)
|
|
29
33
|
current_batch_size: int # Batch size for current attempt
|
|
30
34
|
current_concurrency: int # Concurrency level for current attempt
|
|
31
35
|
|
|
32
36
|
|
|
33
|
-
# Type alias for
|
|
34
|
-
|
|
37
|
+
# Type alias for result callback
|
|
38
|
+
ResultCallback = Callable[[ResultInfo], Awaitable[None]]
|
|
35
39
|
|
|
36
40
|
|
|
37
41
|
def split_into_batches(
|
|
@@ -65,8 +69,9 @@ async def process_batches_concurrently(
|
|
|
65
69
|
operation_func,
|
|
66
70
|
max_concurrent_batches: int,
|
|
67
71
|
total_records: int,
|
|
68
|
-
|
|
72
|
+
on_result: Optional[ResultCallback] = None,
|
|
69
73
|
progress_state: Optional[Dict[str, int]] = None,
|
|
74
|
+
final_results: Optional[List] = None,
|
|
70
75
|
*args,
|
|
71
76
|
**kwargs,
|
|
72
77
|
) -> List[Any]:
|
|
@@ -80,8 +85,8 @@ async def process_batches_concurrently(
|
|
|
80
85
|
:param operation_func: Function to call for each batch
|
|
81
86
|
:param max_concurrent_batches: Maximum number of concurrent batch operations
|
|
82
87
|
:param total_records: Total number of records being processed
|
|
83
|
-
:param
|
|
84
|
-
:param progress_state: Dict with progress state (
|
|
88
|
+
:param on_result: Optional callback invoked after each batch completes with results
|
|
89
|
+
:param progress_state: Dict with progress state (to include in callback)
|
|
85
90
|
:param args: Additional positional arguments for operation_func
|
|
86
91
|
:param kwargs: Additional keyword arguments for operation_func
|
|
87
92
|
:returns: List of results from all batches in the same order as input
|
|
@@ -91,7 +96,7 @@ async def process_batches_concurrently(
|
|
|
91
96
|
raise ValueError("max_concurrent_batches must be greater than 0")
|
|
92
97
|
|
|
93
98
|
semaphore = asyncio.Semaphore(max_concurrent_batches)
|
|
94
|
-
callback_lock = asyncio.Lock() if
|
|
99
|
+
callback_lock = asyncio.Lock() if on_result else None
|
|
95
100
|
|
|
96
101
|
async def process_batch_with_semaphore(batch_index: int, batch):
|
|
97
102
|
async with semaphore:
|
|
@@ -104,10 +109,37 @@ async def process_batches_concurrently(
|
|
|
104
109
|
)
|
|
105
110
|
result = [e for _ in range(len(batch))]
|
|
106
111
|
|
|
107
|
-
# Invoke
|
|
108
|
-
if
|
|
112
|
+
# Invoke callback if provided, with results and progress state
|
|
113
|
+
if on_result and callback_lock and progress_state:
|
|
109
114
|
async with callback_lock:
|
|
110
|
-
|
|
115
|
+
# Split results into successes and errors
|
|
116
|
+
successes: List[CollectionResult] = []
|
|
117
|
+
errors: List[CollectionResult] = []
|
|
118
|
+
|
|
119
|
+
for item in result:
|
|
120
|
+
# Convert exceptions to CollectionResult format
|
|
121
|
+
if isinstance(item, Exception):
|
|
122
|
+
error_result = convert_exception_to_result(item)
|
|
123
|
+
errors.append(error_result)
|
|
124
|
+
elif item.get("success", False):
|
|
125
|
+
successes.append(item)
|
|
126
|
+
else:
|
|
127
|
+
errors.append(item)
|
|
128
|
+
|
|
129
|
+
# Update progress_state cumulatively for per-batch reporting
|
|
130
|
+
# We only count successes as completed here (errors remain pending until final)
|
|
131
|
+
progress_state["records_succeeded"] += len(successes)
|
|
132
|
+
progress_state["records_completed"] += len(successes)
|
|
133
|
+
progress_state["records_pending"] = progress_state[
|
|
134
|
+
"total_records"
|
|
135
|
+
] - (
|
|
136
|
+
progress_state["records_succeeded"]
|
|
137
|
+
+ progress_state["records_failed"]
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
result_info: ResultInfo = {
|
|
141
|
+
"successes": successes,
|
|
142
|
+
"errors": errors,
|
|
111
143
|
"total_records": progress_state["total_records"],
|
|
112
144
|
"records_completed": progress_state["records_completed"],
|
|
113
145
|
"records_succeeded": progress_state["records_succeeded"],
|
|
@@ -117,7 +149,7 @@ async def process_batches_concurrently(
|
|
|
117
149
|
"current_batch_size": progress_state["current_batch_size"],
|
|
118
150
|
"current_concurrency": progress_state["current_concurrency"],
|
|
119
151
|
}
|
|
120
|
-
await
|
|
152
|
+
await on_result(result_info)
|
|
121
153
|
|
|
122
154
|
return result
|
|
123
155
|
|
|
@@ -141,7 +173,7 @@ async def process_with_retries(
|
|
|
141
173
|
max_attempts: int,
|
|
142
174
|
should_retry_callback: Optional[ShouldRetryCallback],
|
|
143
175
|
max_concurrent_batches: Union[int, List[int]],
|
|
144
|
-
|
|
176
|
+
on_result: Optional[ResultCallback],
|
|
145
177
|
max_limit: int,
|
|
146
178
|
*args,
|
|
147
179
|
**kwargs,
|
|
@@ -155,7 +187,7 @@ async def process_with_retries(
|
|
|
155
187
|
:param max_attempts: Maximum number of attempts per record
|
|
156
188
|
:param should_retry_callback: Optional callback to determine if record should be retried
|
|
157
189
|
:param max_concurrent_batches: Maximum concurrent batches (int or list of ints per attempt)
|
|
158
|
-
:param
|
|
190
|
+
:param on_result: Callback invoked after each batch completes with results and progress
|
|
159
191
|
:param max_limit: Maximum batch size limit for the operation
|
|
160
192
|
:param args: Additional args for operation_func
|
|
161
193
|
:param kwargs: Additional kwargs for operation_func
|
|
@@ -203,14 +235,15 @@ async def process_with_retries(
|
|
|
203
235
|
records_to_process = [r.record for r in current_records]
|
|
204
236
|
batches = split_into_batches(records_to_process, current_batch_size, max_limit)
|
|
205
237
|
|
|
206
|
-
# Process batches
|
|
238
|
+
# Process batches - callback will be invoked for each batch with results
|
|
207
239
|
batch_results = await process_batches_concurrently(
|
|
208
240
|
batches,
|
|
209
241
|
operation_func,
|
|
210
242
|
current_concurrency,
|
|
211
243
|
len(records_to_process),
|
|
212
|
-
|
|
213
|
-
|
|
244
|
+
on_result,
|
|
245
|
+
progress_state,
|
|
246
|
+
final_results,
|
|
214
247
|
*args,
|
|
215
248
|
**kwargs,
|
|
216
249
|
)
|
|
@@ -224,11 +257,7 @@ async def process_with_retries(
|
|
|
224
257
|
final_results,
|
|
225
258
|
)
|
|
226
259
|
|
|
227
|
-
# Update progress state based on results
|
|
228
|
-
# Count completed records (those not being retried)
|
|
229
|
-
records_completed_this_round = len(current_records) - len(records_to_retry)
|
|
230
|
-
|
|
231
|
-
# Count successes and failures in final_results so far
|
|
260
|
+
# Update progress state based on results for next iteration
|
|
232
261
|
records_succeeded = sum(
|
|
233
262
|
1 for r in final_results if r is not None and r.get("success", False)
|
|
234
263
|
)
|
|
@@ -241,20 +270,6 @@ async def process_with_retries(
|
|
|
241
270
|
progress_state["records_failed"] = records_failed
|
|
242
271
|
progress_state["records_pending"] = len(records_to_retry)
|
|
243
272
|
|
|
244
|
-
# Invoke progress callback after we know the results
|
|
245
|
-
if on_batch_complete:
|
|
246
|
-
progress_info: ProgressInfo = {
|
|
247
|
-
"total_records": progress_state["total_records"],
|
|
248
|
-
"records_completed": progress_state["records_completed"],
|
|
249
|
-
"records_succeeded": progress_state["records_succeeded"],
|
|
250
|
-
"records_failed": progress_state["records_failed"],
|
|
251
|
-
"records_pending": progress_state["records_pending"],
|
|
252
|
-
"current_attempt": progress_state["current_attempt"],
|
|
253
|
-
"current_batch_size": progress_state["current_batch_size"],
|
|
254
|
-
"current_concurrency": progress_state["current_concurrency"],
|
|
255
|
-
}
|
|
256
|
-
await on_batch_complete(progress_info)
|
|
257
|
-
|
|
258
273
|
if records_to_retry:
|
|
259
274
|
logger.info(
|
|
260
275
|
f"Retrying {len(records_to_retry)} failed records "
|
|
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
|
|
8
8
|
if TYPE_CHECKING:
|
|
9
9
|
from ..client import SalesforceClient
|
|
10
10
|
|
|
11
|
-
from .batch import
|
|
11
|
+
from .batch import ResultCallback, process_with_retries
|
|
12
12
|
from .records import (
|
|
13
13
|
detect_record_type_and_sobject,
|
|
14
14
|
prepare_records,
|
|
@@ -92,7 +92,7 @@ class CollectionsAPI:
|
|
|
92
92
|
batch_size: Union[int, List[int]] = 200,
|
|
93
93
|
max_concurrent_batches: Union[int, List[int]] = 5,
|
|
94
94
|
api_version: Optional[str] = None,
|
|
95
|
-
|
|
95
|
+
on_result: Optional[ResultCallback] = None,
|
|
96
96
|
max_attempts: int = 1,
|
|
97
97
|
should_retry: Optional[ShouldRetryCallback] = None,
|
|
98
98
|
) -> CollectionInsertResponse:
|
|
@@ -109,7 +109,7 @@ class CollectionsAPI:
|
|
|
109
109
|
:param batch_size: Batch size (int for same size, or list of ints per attempt). Max 200.
|
|
110
110
|
:param max_concurrent_batches: Maximum number of concurrent batch operations
|
|
111
111
|
:param api_version: API version to use
|
|
112
|
-
:param
|
|
112
|
+
:param on_result: Optional async callback invoked after each batch completes with results
|
|
113
113
|
:param max_attempts: Maximum number of attempts per record (default: 1, no retries)
|
|
114
114
|
:param should_retry: Optional callback to determine if a failed record should be retried
|
|
115
115
|
:returns: List of results for each record, in same order as input
|
|
@@ -139,7 +139,7 @@ class CollectionsAPI:
|
|
|
139
139
|
max_attempts,
|
|
140
140
|
should_retry,
|
|
141
141
|
max_concurrent_batches,
|
|
142
|
-
|
|
142
|
+
on_result,
|
|
143
143
|
self.MAX_RECORDS_INSERT,
|
|
144
144
|
actual_sobject_type,
|
|
145
145
|
all_or_none,
|
|
@@ -200,7 +200,7 @@ class CollectionsAPI:
|
|
|
200
200
|
batch_size: Union[int, List[int]] = 200,
|
|
201
201
|
max_concurrent_batches: Union[int, List[int]] = 5,
|
|
202
202
|
api_version: Optional[str] = None,
|
|
203
|
-
|
|
203
|
+
on_result: Optional[ResultCallback] = None,
|
|
204
204
|
max_attempts: int = 1,
|
|
205
205
|
should_retry: Optional[ShouldRetryCallback] = None,
|
|
206
206
|
) -> CollectionUpdateResponse:
|
|
@@ -217,7 +217,7 @@ class CollectionsAPI:
|
|
|
217
217
|
:param batch_size: Batch size (int for same size, or list of ints per attempt). Max 200.
|
|
218
218
|
:param max_concurrent_batches: Maximum number of concurrent batch operations
|
|
219
219
|
:param api_version: API version to use
|
|
220
|
-
:param
|
|
220
|
+
:param on_result: Optional async callback invoked after each batch completes with results
|
|
221
221
|
:param max_attempts: Maximum number of attempts per record (default: 1, no retries)
|
|
222
222
|
:param should_retry: Optional callback to determine if a failed record should be retried
|
|
223
223
|
:returns: List of results for each record, in same order as input
|
|
@@ -247,7 +247,7 @@ class CollectionsAPI:
|
|
|
247
247
|
max_attempts,
|
|
248
248
|
should_retry,
|
|
249
249
|
max_concurrent_batches,
|
|
250
|
-
|
|
250
|
+
on_result,
|
|
251
251
|
self.MAX_RECORDS_UPDATE,
|
|
252
252
|
actual_sobject_type,
|
|
253
253
|
all_or_none,
|
|
@@ -313,7 +313,7 @@ class CollectionsAPI:
|
|
|
313
313
|
batch_size: Union[int, List[int]] = 200,
|
|
314
314
|
max_concurrent_batches: Union[int, List[int]] = 5,
|
|
315
315
|
api_version: Optional[str] = None,
|
|
316
|
-
|
|
316
|
+
on_result: Optional[ResultCallback] = None,
|
|
317
317
|
max_attempts: int = 1,
|
|
318
318
|
should_retry: Optional[ShouldRetryCallback] = None,
|
|
319
319
|
) -> CollectionUpsertResponse:
|
|
@@ -331,7 +331,7 @@ class CollectionsAPI:
|
|
|
331
331
|
:param batch_size: Batch size (int for same size, or list of ints per attempt). Max 200.
|
|
332
332
|
:param max_concurrent_batches: Maximum number of concurrent batch operations
|
|
333
333
|
:param api_version: API version to use
|
|
334
|
-
:param
|
|
334
|
+
:param on_result: Optional async callback invoked after each batch completes with results
|
|
335
335
|
:param max_attempts: Maximum number of attempts per record (default: 1, no retries)
|
|
336
336
|
:param should_retry: Optional callback to determine if a failed record should be retried
|
|
337
337
|
:returns: List of results for each record, in same order as input
|
|
@@ -361,7 +361,7 @@ class CollectionsAPI:
|
|
|
361
361
|
max_attempts,
|
|
362
362
|
should_retry,
|
|
363
363
|
max_concurrent_batches,
|
|
364
|
-
|
|
364
|
+
on_result,
|
|
365
365
|
self.MAX_RECORDS_UPSERT,
|
|
366
366
|
external_id_field,
|
|
367
367
|
actual_sobject_type,
|
|
@@ -411,7 +411,7 @@ class CollectionsAPI:
|
|
|
411
411
|
batch_size: Union[int, List[int]] = 200,
|
|
412
412
|
max_concurrent_batches: Union[int, List[int]] = 5,
|
|
413
413
|
api_version: Optional[str] = None,
|
|
414
|
-
|
|
414
|
+
on_result: Optional[ResultCallback] = None,
|
|
415
415
|
max_attempts: int = 1,
|
|
416
416
|
should_retry: Optional[ShouldRetryCallback] = None,
|
|
417
417
|
) -> CollectionDeleteResponse:
|
|
@@ -427,7 +427,7 @@ class CollectionsAPI:
|
|
|
427
427
|
:param batch_size: Batch size (int for same size, or list of ints per attempt). Max 200.
|
|
428
428
|
:param max_concurrent_batches: Maximum number of concurrent batch operations
|
|
429
429
|
:param api_version: API version to use
|
|
430
|
-
:param
|
|
430
|
+
:param on_result: Optional async callback invoked after each batch completes with results
|
|
431
431
|
:param max_attempts: Maximum number of attempts per record (default: 1, no retries)
|
|
432
432
|
:param should_retry: Optional callback to determine if a failed record should be retried
|
|
433
433
|
:returns: List of results for each record, in same order as input
|
|
@@ -453,7 +453,7 @@ class CollectionsAPI:
|
|
|
453
453
|
max_attempts,
|
|
454
454
|
should_retry,
|
|
455
455
|
max_concurrent_batches,
|
|
456
|
-
|
|
456
|
+
on_result,
|
|
457
457
|
self.MAX_RECORDS_DELETE,
|
|
458
458
|
all_or_none,
|
|
459
459
|
api_version,
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import pytest
|
|
4
4
|
import asyncio
|
|
5
|
-
from aio_sf.api.collections import CollectionsAPI,
|
|
5
|
+
from aio_sf.api.collections import CollectionsAPI, ResultInfo
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class MockClient:
|
|
@@ -463,31 +463,38 @@ class TestProgressTracking:
|
|
|
463
463
|
]
|
|
464
464
|
)
|
|
465
465
|
|
|
466
|
-
|
|
466
|
+
result_calls = []
|
|
467
467
|
|
|
468
|
-
async def
|
|
469
|
-
|
|
468
|
+
async def result_callback(result: ResultInfo):
|
|
469
|
+
result_calls.append(dict(result))
|
|
470
470
|
|
|
471
471
|
results = await collections_api.insert(
|
|
472
472
|
records,
|
|
473
473
|
sobject_type="Account",
|
|
474
474
|
batch_size=200,
|
|
475
|
-
|
|
475
|
+
on_result=result_callback,
|
|
476
476
|
)
|
|
477
477
|
|
|
478
478
|
assert len(results) == 300
|
|
479
|
-
# With the new design, callback is invoked once per
|
|
480
|
-
#
|
|
481
|
-
assert len(
|
|
482
|
-
|
|
483
|
-
#
|
|
484
|
-
assert
|
|
485
|
-
assert
|
|
486
|
-
assert
|
|
487
|
-
assert
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
assert
|
|
479
|
+
# With the new design, callback is invoked once per batch
|
|
480
|
+
# 300 records with batch_size=200 means 2 batches
|
|
481
|
+
assert len(result_calls) == 2
|
|
482
|
+
|
|
483
|
+
# Both callbacks split successes and errors
|
|
484
|
+
assert len(result_calls[0]["successes"]) == 200
|
|
485
|
+
assert len(result_calls[0]["errors"]) == 0
|
|
486
|
+
assert len(result_calls[1]["successes"]) == 100
|
|
487
|
+
assert len(result_calls[1]["errors"]) == 0
|
|
488
|
+
|
|
489
|
+
# Verify successes are properly typed CollectionResults
|
|
490
|
+
assert all(r.get("success") for r in result_calls[0]["successes"])
|
|
491
|
+
assert all(r.get("id") is not None for r in result_calls[0]["successes"])
|
|
492
|
+
|
|
493
|
+
# Context information is provided
|
|
494
|
+
assert result_calls[0]["total_records"] == 300
|
|
495
|
+
assert result_calls[0]["current_batch_size"] == 200
|
|
496
|
+
assert result_calls[0]["current_concurrency"] == 5
|
|
497
|
+
assert result_calls[0]["current_attempt"] == 1
|
|
491
498
|
|
|
492
499
|
@pytest.mark.asyncio
|
|
493
500
|
async def test_progress_with_retries(self, client):
|
|
@@ -517,36 +524,39 @@ class TestProgressTracking:
|
|
|
517
524
|
]
|
|
518
525
|
)
|
|
519
526
|
|
|
520
|
-
|
|
527
|
+
result_calls = []
|
|
521
528
|
|
|
522
|
-
async def
|
|
523
|
-
|
|
529
|
+
async def result_callback(result: ResultInfo):
|
|
530
|
+
result_calls.append(dict(result))
|
|
524
531
|
|
|
525
532
|
results = await collections_api.insert(
|
|
526
533
|
records,
|
|
527
534
|
sobject_type="Account",
|
|
528
535
|
batch_size=200,
|
|
529
536
|
max_attempts=2,
|
|
530
|
-
|
|
537
|
+
on_result=result_callback,
|
|
531
538
|
)
|
|
532
539
|
|
|
533
540
|
assert len(results) == 10
|
|
534
541
|
assert all(r["success"] for r in results)
|
|
535
542
|
|
|
536
|
-
# Should have 2
|
|
537
|
-
assert len(
|
|
538
|
-
|
|
539
|
-
# First attempt: all failed
|
|
540
|
-
assert
|
|
541
|
-
assert
|
|
542
|
-
assert
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
assert
|
|
543
|
+
# Should have 2 callbacks (initial attempt + retry attempt)
|
|
544
|
+
assert len(result_calls) == 2
|
|
545
|
+
|
|
546
|
+
# First attempt: all failed - errors array contains them
|
|
547
|
+
assert result_calls[0]["current_attempt"] == 1
|
|
548
|
+
assert len(result_calls[0]["successes"]) == 0
|
|
549
|
+
assert len(result_calls[0]["errors"]) == 10
|
|
550
|
+
# Can inspect error codes in errors array
|
|
551
|
+
for error in result_calls[0]["errors"]:
|
|
552
|
+
assert not error.get("success")
|
|
553
|
+
assert error["errors"][0]["statusCode"] == "UNABLE_TO_LOCK_ROW"
|
|
554
|
+
|
|
555
|
+
# Second attempt: all succeeded - successes array contains them
|
|
556
|
+
assert result_calls[1]["current_attempt"] == 2
|
|
557
|
+
assert len(result_calls[1]["successes"]) == 10
|
|
558
|
+
assert len(result_calls[1]["errors"]) == 0
|
|
559
|
+
assert all(r.get("success") for r in result_calls[1]["successes"])
|
|
550
560
|
|
|
551
561
|
|
|
552
562
|
class TestConcurrencyScaling:
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|