|
192 | 192 | BACKOFF_JITTER_SECONDS = 0.1 |
193 | 193 | RETRYABLE_STATUS_CODES = {408, 425, 429, 499} |
194 | 194 | _SUCCESSFUL_DELETION_MESSAGE = "successfully deleted" |
| 195 | +_DEMO_RESTRICTION_MESSAGE_FIELDS = ("message", "warning", "keyMessage") |
| 196 | +_DEMO_FALLBACK_FILE_ID = "00000000-0000-4000-8000-000000000000" |
| 197 | +_DEMO_FALLBACK_FILE_URL = "https://pdfrest.com/demo-redacted" |
| 198 | +_DEMO_FALLBACK_MIME_TYPE = "application/octet-stream" |
| 199 | +_DEMO_FALLBACK_FILE_NAME = "demo-redacted.bin" |
| 200 | +_DEMO_FALLBACK_FILE_SIZE = 1 |
195 | 201 |
|
196 | 202 |
|
197 | 203 | HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"] |
@@ -263,6 +269,17 @@ def _parse_retry_after_header(header_value: str | None) -> float | None: |
263 | 269 | return seconds if seconds > 0 else 0.0 |
264 | 270 |
|
265 | 271 |
|
| 272 | +def _is_demo_restriction_message(value: str) -> bool: |
| 273 | + normalized = value.strip().casefold() |
| 274 | + if not normalized: |
| 275 | + return False |
| 276 | + return ( |
| 277 | + "watermarked or redacted" in normalized |
| 278 | + and "free account" in normalized |
| 279 | + and "upgrade your plan" in normalized |
| 280 | + ) |
| 281 | + |
| 282 | + |
266 | 283 | FileContent = IO[bytes] | bytes | str |
267 | 284 | FileTuple2 = tuple[str | None, FileContent] |
268 | 285 | FileTuple3 = tuple[str | None, FileContent, str | None] |
@@ -308,6 +325,23 @@ def _extract_uploaded_file_ids(payload: Any) -> list[str]: |
308 | 325 | return file_ids |
309 | 326 |
|
310 | 327 |
|
| 328 | +def _is_demo_fallback_file_id(file_id: str) -> bool: |
| 329 | + return file_id.strip().lower() == _DEMO_FALLBACK_FILE_ID |
| 330 | + |
| 331 | + |
| 332 | +def _build_demo_fallback_file(file_id: str) -> PdfRestFile: |
| 333 | + return PdfRestFile.model_validate( |
| 334 | + { |
| 335 | + "id": file_id, |
| 336 | + "name": _DEMO_FALLBACK_FILE_NAME, |
| 337 | + "url": _DEMO_FALLBACK_FILE_URL, |
| 338 | + "type": _DEMO_FALLBACK_MIME_TYPE, |
| 339 | + "size": _DEMO_FALLBACK_FILE_SIZE, |
| 340 | + "modified": datetime.now(timezone.utc), |
| 341 | + } |
| 342 | + ) |
| 343 | + |
| 344 | + |
311 | 345 | def _handle_deletion_failures(response: PdfRestDeletionResponse) -> None: |
312 | 346 | failures: list[PdfRestDeleteError] = [] |
313 | 347 | for file_id, result in response.deletion_responses.items(): |
@@ -836,11 +870,13 @@ def _handle_response(self, response: httpx.Response) -> Any: |
836 | 870 | f"{getattr(request, 'method', 'UNKNOWN')} {getattr(request, 'url', '')}" |
837 | 871 | ) |
838 | 872 | if response.is_success: |
| 873 | + payload = self._decode_json(response) |
| 874 | + self._log_demo_restriction_messages(payload, request_label) |
839 | 875 | if self._logger.isEnabledFor(logging.DEBUG): |
840 | 876 | self._logger.debug( |
841 | 877 | "Response %s status=%s", request_label, response.status_code |
842 | 878 | ) |
843 | | - return self._decode_json(response) |
| 879 | + return payload |
844 | 880 |
|
845 | 881 | message, error_payload = self._extract_error_details(response) |
846 | 882 | retry_after = _parse_retry_after_header(response.headers.get("Retry-After")) |
@@ -888,6 +924,30 @@ def _decode_json(self, response: httpx.Response) -> Any: |
888 | 924 | response_content=response.text, |
889 | 925 | ) from exc |
890 | 926 |
|
| 927 | + def _log_demo_restriction_messages(self, payload: Any, request_label: str) -> None: |
| 928 | + if not isinstance(payload, Mapping): |
| 929 | + return |
| 930 | + |
| 931 | + typed_payload = cast(Mapping[str, Any], payload) |
| 932 | + emitted_messages: set[str] = set() |
| 933 | + for field_name in _DEMO_RESTRICTION_MESSAGE_FIELDS: |
| 934 | + value = typed_payload.get(field_name) |
| 935 | + if not isinstance(value, str): |
| 936 | + continue |
| 937 | + message = value.strip() |
| 938 | + if not _is_demo_restriction_message(message): |
| 939 | + continue |
| 940 | + normalized_message = message.casefold() |
| 941 | + if normalized_message in emitted_messages: |
| 942 | + continue |
| 943 | + emitted_messages.add(normalized_message) |
| 944 | + self._logger.warning( |
| 945 | + "Demo mode restriction message in response %s field=%s: %s", |
| 946 | + request_label, |
| 947 | + field_name, |
| 948 | + message, |
| 949 | + ) |
| 950 | + |
891 | 951 | @staticmethod |
892 | 952 | def _extract_error_details( |
893 | 953 | response: httpx.Response, |
@@ -1160,7 +1220,17 @@ def fetch_file_info( |
1160 | 1220 | extra_headers=extra_headers, |
1161 | 1221 | timeout=timeout, |
1162 | 1222 | ) |
1163 | | - payload = self._send_request(request) |
| 1223 | + try: |
| 1224 | + payload = self._send_request(request) |
| 1225 | + except PdfRestApiError as exc: |
| 1226 | + if exc.status_code == 404 and _is_demo_fallback_file_id(file_id): |
| 1227 | + self._logger.warning( |
| 1228 | + "Demo fallback file id %s was not found during file-info lookup; " |
| 1229 | + "returning placeholder metadata.", |
| 1230 | + file_id, |
| 1231 | + ) |
| 1232 | + return _build_demo_fallback_file(file_id) |
| 1233 | + raise |
1164 | 1234 | return PdfRestFile.model_validate(payload) |
1165 | 1235 |
|
1166 | 1236 |
|
@@ -1435,7 +1505,17 @@ async def fetch_file_info( |
1435 | 1505 | extra_headers=extra_headers, |
1436 | 1506 | timeout=timeout, |
1437 | 1507 | ) |
1438 | | - payload = await self._send_request(request) |
| 1508 | + try: |
| 1509 | + payload = await self._send_request(request) |
| 1510 | + except PdfRestApiError as exc: |
| 1511 | + if exc.status_code == 404 and _is_demo_fallback_file_id(file_id): |
| 1512 | + self._logger.warning( |
| 1513 | + "Demo fallback file id %s was not found during file-info lookup; " |
| 1514 | + "returning placeholder metadata.", |
| 1515 | + file_id, |
| 1516 | + ) |
| 1517 | + return _build_demo_fallback_file(file_id) |
| 1518 | + raise |
1439 | 1519 | return PdfRestFile.model_validate(payload) |
1440 | 1520 |
|
1441 | 1521 |
|
|
0 commit comments