aio-sf 0.1.0b8__tar.gz → 0.1.0b9__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.0b8 → aio_sf-0.1.0b9}/PKG-INFO +26 -10
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/README.md +25 -9
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/collections/batch.py +72 -33
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/tests/test_retry_and_batch.py +25 -12
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/.cursor/rules/api-structure.mdc +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/.cursor/rules/async-patterns.mdc +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/.cursor/rules/project-tooling.mdc +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/.github/workflows/publish.yml +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/.github/workflows/test.yml +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/.gitignore +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/LICENSE +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/RELEASE.md +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/pyproject.toml +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/pytest.ini +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/auth/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/auth/base.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/auth/client_credentials.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/auth/refresh_token.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/auth/sfdx_cli.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/auth/static_token.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/bulk_v2/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/bulk_v2/client.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/bulk_v2/types.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/client.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/collections/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/collections/client.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/collections/records.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/collections/retry.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/collections/types.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/describe/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/describe/client.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/describe/types.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/query/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/query/client.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/query/types.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/api/types.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/exporter/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/exporter/bulk_export.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/src/aio_sf/exporter/parquet_writer.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/tests/__init__.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/tests/conftest.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/tests/test_api_clients.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/tests/test_auth.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/tests/test_client.py +0 -0
- {aio_sf-0.1.0b8 → aio_sf-0.1.0b9}/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.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
|
|
|
@@ -90,24 +90,40 @@ asyncio.run(main())
|
|
|
90
90
|
|
|
91
91
|
### Collections API - Batch Operations
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
Bulk operations (insert, update, upsert, delete) with automatic batching and concurrency.
|
|
94
94
|
|
|
95
|
+
**Basic Usage:**
|
|
95
96
|
```python
|
|
96
97
|
async with SalesforceClient(auth_strategy=auth) as sf:
|
|
97
98
|
records = [{"Name": f"Account {i}"} for i in range(1000)]
|
|
98
99
|
|
|
99
|
-
|
|
100
|
-
results = await sf.collections.insert(
|
|
101
|
-
records, sobject_type="Account",
|
|
102
|
-
batch_size=200, max_concurrent_batches=5
|
|
103
|
-
)
|
|
104
|
-
|
|
100
|
+
results = await sf.collections.insert(records, sobject_type="Account")
|
|
105
101
|
# Also: update(), upsert(), delete()
|
|
106
102
|
```
|
|
107
103
|
|
|
108
|
-
**
|
|
104
|
+
**Advanced - With Retries, Concurrency Scaling, and Progress:**
|
|
105
|
+
```python
|
|
106
|
+
from aio_sf.api.collections import ProgressInfo
|
|
107
|
+
|
|
108
|
+
async def on_progress(info: ProgressInfo):
|
|
109
|
+
print(
|
|
110
|
+
f"Attempt {info['current_attempt']}: "
|
|
111
|
+
f"{info['records_succeeded']} succeeded, "
|
|
112
|
+
f"{info['records_failed']} failed, "
|
|
113
|
+
f"{info['records_pending']} pending"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
async with SalesforceClient(auth_strategy=auth) as sf:
|
|
117
|
+
results = await sf.collections.insert(
|
|
118
|
+
records=records,
|
|
119
|
+
sobject_type="Account",
|
|
120
|
+
batch_size=[200, 100, 25], # Shrink batch size on retry
|
|
121
|
+
max_concurrent_batches=[5, 3, 1], # Reduce concurrency on retry
|
|
122
|
+
max_attempts=5, # Retry up to 5 times
|
|
123
|
+
on_batch_complete=on_progress, # Progress callback
|
|
124
|
+
)
|
|
125
|
+
```
|
|
109
126
|
|
|
110
|
-
See [RETRY_GUIDE.md](RETRY_GUIDE.md) for advanced retry strategies, progress tracking, and custom error handling.
|
|
111
127
|
|
|
112
128
|
## Exporter
|
|
113
129
|
|
|
@@ -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
|
|
|
@@ -476,13 +476,18 @@ class TestProgressTracking:
|
|
|
476
476
|
)
|
|
477
477
|
|
|
478
478
|
assert len(results) == 300
|
|
479
|
-
#
|
|
480
|
-
|
|
479
|
+
# With the new design, callback is invoked once per attempt (after all batches)
|
|
480
|
+
# Since all records succeed on first attempt, we get 1 callback
|
|
481
|
+
assert len(progress_calls) == 1
|
|
481
482
|
|
|
482
|
-
# Verify progress data
|
|
483
|
-
assert progress_calls[0]["total_batches"] == 2
|
|
483
|
+
# Verify progress data - after attempt completes, all succeeded
|
|
484
484
|
assert progress_calls[0]["total_records"] == 300
|
|
485
|
-
assert progress_calls[0]["
|
|
485
|
+
assert progress_calls[0]["current_batch_size"] == 200
|
|
486
|
+
assert progress_calls[0]["current_concurrency"] == 5 # default
|
|
487
|
+
assert progress_calls[0]["current_attempt"] == 1
|
|
488
|
+
assert progress_calls[0]["records_succeeded"] == 300
|
|
489
|
+
assert progress_calls[0]["records_failed"] == 0
|
|
490
|
+
assert progress_calls[0]["records_pending"] == 0
|
|
486
491
|
|
|
487
492
|
@pytest.mark.asyncio
|
|
488
493
|
async def test_progress_with_retries(self, client):
|
|
@@ -530,10 +535,18 @@ class TestProgressTracking:
|
|
|
530
535
|
|
|
531
536
|
# Should have 2 progress callbacks (initial + retry)
|
|
532
537
|
assert len(progress_calls) == 2
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
assert progress_calls[
|
|
538
|
+
|
|
539
|
+
# First attempt: all failed, all pending retry
|
|
540
|
+
assert progress_calls[0]["current_attempt"] == 1
|
|
541
|
+
assert progress_calls[0]["records_succeeded"] == 0
|
|
542
|
+
assert progress_calls[0]["records_failed"] == 0
|
|
543
|
+
assert progress_calls[0]["records_pending"] == 10
|
|
544
|
+
|
|
545
|
+
# Second attempt: all succeeded
|
|
546
|
+
assert progress_calls[1]["current_attempt"] == 2
|
|
547
|
+
assert progress_calls[1]["records_succeeded"] == 10
|
|
548
|
+
assert progress_calls[1]["records_failed"] == 0
|
|
549
|
+
assert progress_calls[1]["records_pending"] == 0
|
|
537
550
|
|
|
538
551
|
|
|
539
552
|
class TestConcurrencyScaling:
|
|
@@ -646,7 +659,7 @@ class TestHTTPErrorHandling:
|
|
|
646
659
|
async def test_default_retries_transient_http_errors(self, client):
|
|
647
660
|
"""Test that default behavior retries transient HTTP errors."""
|
|
648
661
|
import httpx
|
|
649
|
-
|
|
662
|
+
|
|
650
663
|
collections_api = CollectionsAPI(client)
|
|
651
664
|
|
|
652
665
|
records = [{"Name": "Account 1"}]
|
|
@@ -681,7 +694,7 @@ class TestHTTPErrorHandling:
|
|
|
681
694
|
async def test_default_does_not_retry_4xx_errors(self, client):
|
|
682
695
|
"""Test that default behavior does NOT retry 4xx client errors."""
|
|
683
696
|
import httpx
|
|
684
|
-
|
|
697
|
+
|
|
685
698
|
collections_api = CollectionsAPI(client)
|
|
686
699
|
|
|
687
700
|
records = [{"Name": "Account 1"}]
|
|
@@ -716,7 +729,7 @@ class TestHTTPErrorHandling:
|
|
|
716
729
|
async def test_http_error_converts_to_retryable_failure(self, client):
|
|
717
730
|
"""Test that transient HTTP errors are retried and converted to results."""
|
|
718
731
|
import httpx
|
|
719
|
-
|
|
732
|
+
|
|
720
733
|
collections_api = CollectionsAPI(client)
|
|
721
734
|
|
|
722
735
|
records = [{"Name": "Account 1"}, {"Name": "Account 2"}]
|
|
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
|
|
File without changes
|
|
File without changes
|