Skip to content

Commit aba2d2d

Browse files
create_from_paths: Allow complete retry
- Introduced `send_request_once` and `run_with_retry` for consistent request execution and retry handling. - Implemented retries for `create_from_paths` in both sync and async clients, covering 429, transport, and timeout errors. - Added concurrency control using semaphores for async file info fetches after uploads. - Updated tests to validate retry behavior, including various error scenarios and proper retry counts. Assisted-by: Codex
1 parent b45a81f commit aba2d2d

2 files changed

Lines changed: 377 additions & 38 deletions

File tree

src/pdfrest/client.py

Lines changed: 116 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -896,6 +896,22 @@ def _send_request(self, request: _RequestModel) -> Any:
896896
should_continue=stream_retry_checker,
897897
)
898898

899+
def send_request_once(self, request: _RequestModel) -> Any:
900+
return self._perform_request(self._client, request)
901+
902+
def run_with_retry(
903+
self,
904+
func: Callable[[], ReturnType],
905+
*,
906+
operation: str,
907+
should_continue: Callable[[PdfRestError], bool] | None = None,
908+
) -> ReturnType:
909+
return self._execute_with_retry(
910+
func,
911+
operation=operation,
912+
should_continue=should_continue,
913+
)
914+
899915
def _perform_request(
900916
self, http_client: httpx.Client, request: _RequestModel
901917
) -> Any:
@@ -1143,6 +1159,22 @@ async def _send_request(self, request: _RequestModel) -> Any:
11431159
should_continue=stream_retry_checker,
11441160
)
11451161

1162+
async def send_request_once(self, request: _RequestModel) -> Any:
1163+
return await self._perform_request(self._client, request)
1164+
1165+
async def run_with_retry(
1166+
self,
1167+
func: Callable[[], Awaitable[ReturnType]],
1168+
*,
1169+
operation: str,
1170+
should_continue: Callable[[PdfRestError], bool] | None = None,
1171+
) -> ReturnType:
1172+
return await self._execute_with_retry(
1173+
func,
1174+
operation=operation,
1175+
should_continue=should_continue,
1176+
)
1177+
11461178
async def _perform_request(
11471179
self, http_client: httpx.AsyncClient, request: _RequestModel
11481180
) -> Any:
@@ -1441,25 +1473,42 @@ def create_from_paths(
14411473
closed once the request completes.
14421474
"""
14431475
normalized_paths = _normalize_path_inputs(file_paths)
1444-
1445-
with ExitStack() as stack:
1446-
upload_specs: list[FileTypes] = []
1447-
for spec in normalized_paths:
1448-
path, content_type, headers = _parse_path_spec(spec)
1449-
file_obj = stack.enter_context(path.open("rb"))
1450-
filename = path.name
1451-
if headers:
1452-
upload_specs.append((filename, file_obj, content_type, headers))
1453-
elif content_type is not None:
1454-
upload_specs.append((filename, file_obj, content_type))
1455-
else:
1456-
upload_specs.append((filename, file_obj))
1457-
return self.create(
1458-
upload_specs,
1459-
extra_query=extra_query,
1460-
extra_headers=extra_headers,
1461-
timeout=timeout,
1462-
)
1476+
path_specs = [_parse_path_spec(spec) for spec in normalized_paths]
1477+
1478+
def attempt() -> list[PdfRestFile]:
1479+
with ExitStack() as stack:
1480+
upload_specs: list[FileTypes] = []
1481+
for path, content_type, headers in path_specs:
1482+
file_obj = stack.enter_context(path.open("rb"))
1483+
filename = path.name
1484+
if headers:
1485+
upload_specs.append((filename, file_obj, content_type, headers))
1486+
elif content_type is not None:
1487+
upload_specs.append((filename, file_obj, content_type))
1488+
else:
1489+
upload_specs.append((filename, file_obj))
1490+
1491+
request = self._client.prepare_request(
1492+
"POST",
1493+
"/upload",
1494+
files=upload_specs,
1495+
extra_query=extra_query,
1496+
extra_headers=extra_headers,
1497+
timeout=timeout,
1498+
)
1499+
payload = self._client.send_request_once(request)
1500+
file_ids = _extract_uploaded_file_ids(payload)
1501+
return [
1502+
self._client.fetch_file_info(
1503+
file_id,
1504+
extra_query=extra_query,
1505+
extra_headers=extra_headers,
1506+
timeout=timeout,
1507+
)
1508+
for file_id in file_ids
1509+
]
1510+
1511+
return self._client.run_with_retry(attempt, operation="POST /upload (paths)")
14631512

14641513
def create_from_urls(
14651514
self,
@@ -1681,25 +1730,54 @@ async def create_from_paths(
16811730
closed once the request completes.
16821731
"""
16831732
normalized_paths = _normalize_path_inputs(file_paths)
1684-
1685-
with ExitStack() as stack:
1686-
upload_specs: list[FileTypes] = []
1687-
for spec in normalized_paths:
1688-
path, content_type, headers = _parse_path_spec(spec)
1689-
file_obj = stack.enter_context(path.open("rb"))
1690-
filename = path.name
1691-
if headers:
1692-
upload_specs.append((filename, file_obj, content_type, headers))
1693-
elif content_type is not None:
1694-
upload_specs.append((filename, file_obj, content_type))
1695-
else:
1696-
upload_specs.append((filename, file_obj))
1697-
return await self.create(
1698-
upload_specs,
1699-
extra_query=extra_query,
1700-
extra_headers=extra_headers,
1701-
timeout=timeout,
1702-
)
1733+
path_specs = [_parse_path_spec(spec) for spec in normalized_paths]
1734+
1735+
async def attempt() -> list[PdfRestFile]:
1736+
with ExitStack() as stack:
1737+
upload_specs: list[FileTypes] = []
1738+
for path, content_type, headers in path_specs:
1739+
file_obj = stack.enter_context(path.open("rb"))
1740+
filename = path.name
1741+
if headers:
1742+
upload_specs.append((filename, file_obj, content_type, headers))
1743+
elif content_type is not None:
1744+
upload_specs.append((filename, file_obj, content_type))
1745+
else:
1746+
upload_specs.append((filename, file_obj))
1747+
1748+
request = self._client.prepare_request(
1749+
"POST",
1750+
"/upload",
1751+
files=upload_specs,
1752+
extra_query=extra_query,
1753+
extra_headers=extra_headers,
1754+
timeout=timeout,
1755+
)
1756+
payload = await self._client.send_request_once(request)
1757+
file_ids = _extract_uploaded_file_ids(payload)
1758+
results: list[PdfRestFile] = []
1759+
semaphore = asyncio.Semaphore(self._concurrency_limit)
1760+
1761+
async def throttled_fetch(file_id: str) -> PdfRestFile:
1762+
async with semaphore:
1763+
return await self._client.fetch_file_info(
1764+
file_id,
1765+
extra_query=extra_query,
1766+
extra_headers=extra_headers,
1767+
timeout=timeout,
1768+
)
1769+
1770+
if file_ids:
1771+
results = list(
1772+
await asyncio.gather(
1773+
*(throttled_fetch(fid) for fid in file_ids)
1774+
)
1775+
)
1776+
return results
1777+
1778+
return await self._client.run_with_retry(
1779+
attempt, operation="POST /upload (paths)"
1780+
)
17031781

17041782
async def create_from_urls(
17051783
self,

0 commit comments

Comments
 (0)