aio-sf 0.1.0b8__py3-none-any.whl → 0.1.0b9__py3-none-any.whl
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/api/collections/batch.py +72 -33
- {aio_sf-0.1.0b8.dist-info → aio_sf-0.1.0b9.dist-info}/METADATA +26 -10
- {aio_sf-0.1.0b8.dist-info → aio_sf-0.1.0b9.dist-info}/RECORD +5 -5
- {aio_sf-0.1.0b8.dist-info → aio_sf-0.1.0b9.dist-info}/WHEEL +0 -0
- {aio_sf-0.1.0b8.dist-info → aio_sf-0.1.0b9.dist-info}/licenses/LICENSE +0 -0
aio_sf/api/collections/batch.py
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import logging
|
|
5
|
-
from typing import Any, Awaitable, Callable, List, Optional, TypedDict, Union
|
|
5
|
+
from typing import Any, Awaitable, Callable, Dict, List, Optional, TypedDict, Union
|
|
6
6
|
|
|
7
7
|
from .retry import (
|
|
8
8
|
RecordWithAttempt,
|
|
@@ -20,12 +20,14 @@ logger = logging.getLogger(__name__)
|
|
|
20
20
|
class ProgressInfo(TypedDict):
|
|
21
21
|
"""Progress information for batch operations."""
|
|
22
22
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
23
|
+
total_records: int # Total records being processed
|
|
24
|
+
records_completed: int # Records finished (succeeded or failed permanently)
|
|
25
|
+
records_succeeded: int # Records that succeeded
|
|
26
|
+
records_failed: int # Records that failed permanently (exhausted retries)
|
|
27
|
+
records_pending: int # Records still being retried
|
|
28
|
+
current_attempt: int # Current retry attempt number (1-indexed)
|
|
29
|
+
current_batch_size: int # Batch size for current attempt
|
|
30
|
+
current_concurrency: int # Concurrency level for current attempt
|
|
29
31
|
|
|
30
32
|
|
|
31
33
|
# Type alias for progress callback
|
|
@@ -64,6 +66,7 @@ async def process_batches_concurrently(
|
|
|
64
66
|
max_concurrent_batches: int,
|
|
65
67
|
total_records: int,
|
|
66
68
|
on_batch_complete: Optional[ProgressCallback] = None,
|
|
69
|
+
progress_state: Optional[Dict[str, int]] = None,
|
|
67
70
|
*args,
|
|
68
71
|
**kwargs,
|
|
69
72
|
) -> List[Any]:
|
|
@@ -78,6 +81,7 @@ async def process_batches_concurrently(
|
|
|
78
81
|
:param max_concurrent_batches: Maximum number of concurrent batch operations
|
|
79
82
|
:param total_records: Total number of records being processed
|
|
80
83
|
:param on_batch_complete: Optional callback invoked after each batch completes
|
|
84
|
+
:param progress_state: Dict with progress state (updated by caller)
|
|
81
85
|
:param args: Additional positional arguments for operation_func
|
|
82
86
|
:param kwargs: Additional keyword arguments for operation_func
|
|
83
87
|
:returns: List of results from all batches in the same order as input
|
|
@@ -87,12 +91,9 @@ async def process_batches_concurrently(
|
|
|
87
91
|
raise ValueError("max_concurrent_batches must be greater than 0")
|
|
88
92
|
|
|
89
93
|
semaphore = asyncio.Semaphore(max_concurrent_batches)
|
|
90
|
-
total_batches = len(batches)
|
|
91
94
|
callback_lock = asyncio.Lock() if on_batch_complete else None
|
|
92
|
-
records_processed = 0
|
|
93
95
|
|
|
94
96
|
async def process_batch_with_semaphore(batch_index: int, batch):
|
|
95
|
-
nonlocal records_processed
|
|
96
97
|
async with semaphore:
|
|
97
98
|
try:
|
|
98
99
|
result = await operation_func(batch, *args, **kwargs)
|
|
@@ -104,17 +105,17 @@ async def process_batches_concurrently(
|
|
|
104
105
|
result = [e for _ in range(len(batch))]
|
|
105
106
|
|
|
106
107
|
# Invoke progress callback if provided
|
|
107
|
-
if on_batch_complete and callback_lock:
|
|
108
|
-
batch_size = len(batch)
|
|
108
|
+
if on_batch_complete and callback_lock and progress_state:
|
|
109
109
|
async with callback_lock:
|
|
110
|
-
records_processed += batch_size
|
|
111
110
|
progress_info: ProgressInfo = {
|
|
112
|
-
"
|
|
113
|
-
"
|
|
114
|
-
"
|
|
115
|
-
"
|
|
116
|
-
"
|
|
117
|
-
"
|
|
111
|
+
"total_records": progress_state["total_records"],
|
|
112
|
+
"records_completed": progress_state["records_completed"],
|
|
113
|
+
"records_succeeded": progress_state["records_succeeded"],
|
|
114
|
+
"records_failed": progress_state["records_failed"],
|
|
115
|
+
"records_pending": progress_state["records_pending"],
|
|
116
|
+
"current_attempt": progress_state["current_attempt"],
|
|
117
|
+
"current_batch_size": progress_state["current_batch_size"],
|
|
118
|
+
"current_concurrency": progress_state["current_concurrency"],
|
|
118
119
|
}
|
|
119
120
|
await on_batch_complete(progress_info)
|
|
120
121
|
|
|
@@ -163,9 +164,21 @@ async def process_with_retries(
|
|
|
163
164
|
# Initialize result array with None placeholders
|
|
164
165
|
max_index = max(r.original_index for r in records_with_attempts)
|
|
165
166
|
final_results: List[Optional[CollectionResult]] = [None] * (max_index + 1)
|
|
167
|
+
total_records_count = max_index + 1
|
|
168
|
+
|
|
169
|
+
# Initialize progress state
|
|
170
|
+
progress_state = {
|
|
171
|
+
"total_records": total_records_count,
|
|
172
|
+
"records_completed": 0,
|
|
173
|
+
"records_succeeded": 0,
|
|
174
|
+
"records_failed": 0,
|
|
175
|
+
"records_pending": total_records_count,
|
|
176
|
+
"current_attempt": 1,
|
|
177
|
+
"current_batch_size": 0,
|
|
178
|
+
"current_concurrency": 0,
|
|
179
|
+
}
|
|
166
180
|
|
|
167
181
|
current_records = records_with_attempts
|
|
168
|
-
total_retry_count = 0
|
|
169
182
|
|
|
170
183
|
while current_records:
|
|
171
184
|
current_attempt = current_records[0].attempt
|
|
@@ -176,6 +189,11 @@ async def process_with_retries(
|
|
|
176
189
|
current_attempt, max_concurrent_batches
|
|
177
190
|
)
|
|
178
191
|
|
|
192
|
+
# Update progress state for current attempt
|
|
193
|
+
progress_state["current_attempt"] = current_attempt
|
|
194
|
+
progress_state["current_batch_size"] = current_batch_size
|
|
195
|
+
progress_state["current_concurrency"] = current_concurrency
|
|
196
|
+
|
|
179
197
|
logger.debug(
|
|
180
198
|
f"Processing {len(current_records)} records on attempt {current_attempt} "
|
|
181
199
|
f"with batch_size={current_batch_size}, concurrency={current_concurrency}"
|
|
@@ -185,23 +203,14 @@ async def process_with_retries(
|
|
|
185
203
|
records_to_process = [r.record for r in current_records]
|
|
186
204
|
batches = split_into_batches(records_to_process, current_batch_size, max_limit)
|
|
187
205
|
|
|
188
|
-
#
|
|
189
|
-
wrapped_callback = None
|
|
190
|
-
if on_batch_complete:
|
|
191
|
-
|
|
192
|
-
async def progress_wrapper(progress: ProgressInfo):
|
|
193
|
-
progress["retry_count"] = total_retry_count
|
|
194
|
-
await on_batch_complete(progress)
|
|
195
|
-
|
|
196
|
-
wrapped_callback = progress_wrapper
|
|
197
|
-
|
|
198
|
-
# Process batches with current concurrency level
|
|
206
|
+
# Process batches with current concurrency level (no callback here)
|
|
199
207
|
batch_results = await process_batches_concurrently(
|
|
200
208
|
batches,
|
|
201
209
|
operation_func,
|
|
202
210
|
current_concurrency,
|
|
203
211
|
len(records_to_process),
|
|
204
|
-
|
|
212
|
+
None, # Don't invoke callback during batch processing
|
|
213
|
+
None,
|
|
205
214
|
*args,
|
|
206
215
|
**kwargs,
|
|
207
216
|
)
|
|
@@ -215,12 +224,42 @@ async def process_with_retries(
|
|
|
215
224
|
final_results,
|
|
216
225
|
)
|
|
217
226
|
|
|
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
|
|
232
|
+
records_succeeded = sum(
|
|
233
|
+
1 for r in final_results if r is not None and r.get("success", False)
|
|
234
|
+
)
|
|
235
|
+
records_failed = sum(
|
|
236
|
+
1 for r in final_results if r is not None and not r.get("success", False)
|
|
237
|
+
)
|
|
238
|
+
|
|
239
|
+
progress_state["records_completed"] = records_succeeded + records_failed
|
|
240
|
+
progress_state["records_succeeded"] = records_succeeded
|
|
241
|
+
progress_state["records_failed"] = records_failed
|
|
242
|
+
progress_state["records_pending"] = len(records_to_retry)
|
|
243
|
+
|
|
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
|
+
|
|
218
258
|
if records_to_retry:
|
|
219
259
|
logger.info(
|
|
220
260
|
f"Retrying {len(records_to_retry)} failed records "
|
|
221
261
|
f"(attempt {records_to_retry[0].attempt})"
|
|
222
262
|
)
|
|
223
|
-
total_retry_count += len(records_to_retry)
|
|
224
263
|
|
|
225
264
|
current_records = records_to_retry
|
|
226
265
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: aio-sf
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.0b9
|
|
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
|
|
@@ -153,24 +153,40 @@ asyncio.run(main())
|
|
|
153
153
|
|
|
154
154
|
### Collections API - Batch Operations
|
|
155
155
|
|
|
156
|
-
|
|
156
|
+
Bulk operations (insert, update, upsert, delete) with automatic batching and concurrency.
|
|
157
157
|
|
|
158
|
+
**Basic Usage:**
|
|
158
159
|
```python
|
|
159
160
|
async with SalesforceClient(auth_strategy=auth) as sf:
|
|
160
161
|
records = [{"Name": f"Account {i}"} for i in range(1000)]
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
results = await sf.collections.insert(
|
|
164
|
-
records, sobject_type="Account",
|
|
165
|
-
batch_size=200, max_concurrent_batches=5
|
|
166
|
-
)
|
|
167
|
-
|
|
163
|
+
results = await sf.collections.insert(records, sobject_type="Account")
|
|
168
164
|
# Also: update(), upsert(), delete()
|
|
169
165
|
```
|
|
170
166
|
|
|
171
|
-
**
|
|
167
|
+
**Advanced - With Retries, Concurrency Scaling, and Progress:**
|
|
168
|
+
```python
|
|
169
|
+
from aio_sf.api.collections import ProgressInfo
|
|
170
|
+
|
|
171
|
+
async def on_progress(info: ProgressInfo):
|
|
172
|
+
print(
|
|
173
|
+
f"Attempt {info['current_attempt']}: "
|
|
174
|
+
f"{info['records_succeeded']} succeeded, "
|
|
175
|
+
f"{info['records_failed']} failed, "
|
|
176
|
+
f"{info['records_pending']} pending"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
async with SalesforceClient(auth_strategy=auth) as sf:
|
|
180
|
+
results = await sf.collections.insert(
|
|
181
|
+
records=records,
|
|
182
|
+
sobject_type="Account",
|
|
183
|
+
batch_size=[200, 100, 25], # Shrink batch size on retry
|
|
184
|
+
max_concurrent_batches=[5, 3, 1], # Reduce concurrency on retry
|
|
185
|
+
max_attempts=5, # Retry up to 5 times
|
|
186
|
+
on_batch_complete=on_progress, # Progress callback
|
|
187
|
+
)
|
|
188
|
+
```
|
|
172
189
|
|
|
173
|
-
See [RETRY_GUIDE.md](RETRY_GUIDE.md) for advanced retry strategies, progress tracking, and custom error handling.
|
|
174
190
|
|
|
175
191
|
## Exporter
|
|
176
192
|
|
|
@@ -12,7 +12,7 @@ aio_sf/api/bulk_v2/__init__.py,sha256=TxNM9dFmRX5k57Wj_JnposeEqqd9xcJx78wI-d6VH0
|
|
|
12
12
|
aio_sf/api/bulk_v2/client.py,sha256=t2vl8bIIvSrbg5T1mFDm7O4vq0GyDihfkVHamGYnZ7I,6817
|
|
13
13
|
aio_sf/api/bulk_v2/types.py,sha256=18TN_VMisKJVCvo39q36moYXQhGLRUs73CKWIxBeAcs,1471
|
|
14
14
|
aio_sf/api/collections/__init__.py,sha256=kE9NTBr-iBgQy-ZQAkJD92m7wYk7FQQ-K0gcKnvUoIg,685
|
|
15
|
-
aio_sf/api/collections/batch.py,sha256=
|
|
15
|
+
aio_sf/api/collections/batch.py,sha256=2Ie6qeIR-INe4q3c8uoFuXfTonW9bLTDsACuIIqgmAM,14009
|
|
16
16
|
aio_sf/api/collections/client.py,sha256=IhIn6D-PpunSeztueK4Rs7AIHp57vhNyH3CuvcXnR6c,19098
|
|
17
17
|
aio_sf/api/collections/records.py,sha256=UwI4mjoMkJPBCYbsNUmj5HTBjbGdcaMHT6UiJ4EmcUA,5371
|
|
18
18
|
aio_sf/api/collections/retry.py,sha256=RXTpMhWyeYsXNiojgEYIVnGtg8S0iYMxgfWgX3APahE,4669
|
|
@@ -26,7 +26,7 @@ aio_sf/api/query/types.py,sha256=Wfk75kJpNDCGpTHonCbzjWvayy8guA3eyZp3hE7nBt0,845
|
|
|
26
26
|
aio_sf/exporter/__init__.py,sha256=waTegrvw_SvJzREAWD4twSDldSL-HfvhLTLLT1o765o,771
|
|
27
27
|
aio_sf/exporter/bulk_export.py,sha256=2GtiwXChf7dq7dByGLPDhIJJg-yq9eyoE57H4Ekqaus,13169
|
|
28
28
|
aio_sf/exporter/parquet_writer.py,sha256=jGiLooxyaqciSDUbXj5F_4uWoR_YrQaB-PrDfRuXR3Y,14495
|
|
29
|
-
aio_sf-0.1.
|
|
30
|
-
aio_sf-0.1.
|
|
31
|
-
aio_sf-0.1.
|
|
32
|
-
aio_sf-0.1.
|
|
29
|
+
aio_sf-0.1.0b9.dist-info/METADATA,sha256=ofb2S9YAaOQMkydePRKMeGkMwxtahx5AQyL-Pv9rDsQ,7991
|
|
30
|
+
aio_sf-0.1.0b9.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
|
|
31
|
+
aio_sf-0.1.0b9.dist-info/licenses/LICENSE,sha256=gu0Cbpiqs-vX7YgJJhGI1jH1mHup3dZMrZc-gmpEG60,1071
|
|
32
|
+
aio_sf-0.1.0b9.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|