Skip to content

Commit d55e163

Browse files
client: Log and test demo restriction messages in API responses
- Added `_log_demo_restriction_messages` to log when API responses include demo mode restriction messages in specific fields (`message`, `warning`, `keyMessage`) for both sync and async clients. - Introduced `_is_demo_restriction_message` utility to identify demo-related restriction messages based on known patterns. - Updated documentation in `getting-started.md` with examples of these log messages and how to configure logging for monitoring. - Created new test cases to validate the detection and logging of demo restriction messages: - Tests ensure messages are logged once even if duplicated across fields. - Verified async and sync clients handle and log these cases consistently. Assisted-by: Codex
1 parent fcbb8b2 commit d55e163

3 files changed

Lines changed: 206 additions & 1 deletion

File tree

docs/getting-started.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,13 +90,19 @@ When a replacement happens, the SDK logs a warning in this format:
9090

9191
`Demo value <val> detected in <field-name>; replaced with <replacement>`
9292

93+
When the API returns a demo restriction body message (for example the free-tier
94+
"watermarked or redacted" notice in `message`), the SDK also logs:
95+
96+
`Demo mode restriction message in response <METHOD URL> field=<field>: <message>`
97+
9398
To see these warnings in your app, configure Python logging (example):
9499

95100
```python
96101
import logging
97102

98103
logging.basicConfig(level=logging.WARNING)
99104
logging.getLogger("pdfrest.models").setLevel(logging.WARNING)
105+
logging.getLogger("pdfrest.client").setLevel(logging.WARNING)
100106
```
101107

102108
## 3. Add a short example program

src/pdfrest/client.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@
192192
BACKOFF_JITTER_SECONDS = 0.1
193193
RETRYABLE_STATUS_CODES = {408, 425, 429, 499}
194194
_SUCCESSFUL_DELETION_MESSAGE = "successfully deleted"
195+
_DEMO_RESTRICTION_MESSAGE_FIELDS = ("message", "warning", "keyMessage")
195196

196197

197198
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
@@ -263,6 +264,17 @@ def _parse_retry_after_header(header_value: str | None) -> float | None:
263264
return seconds if seconds > 0 else 0.0
264265

265266

267+
def _is_demo_restriction_message(value: str) -> bool:
268+
normalized = value.strip().casefold()
269+
if not normalized:
270+
return False
271+
return (
272+
"watermarked or redacted" in normalized
273+
and "free account" in normalized
274+
and "upgrade your plan" in normalized
275+
)
276+
277+
266278
FileContent = IO[bytes] | bytes | str
267279
FileTuple2 = tuple[str | None, FileContent]
268280
FileTuple3 = tuple[str | None, FileContent, str | None]
@@ -836,11 +848,13 @@ def _handle_response(self, response: httpx.Response) -> Any:
836848
f"{getattr(request, 'method', 'UNKNOWN')} {getattr(request, 'url', '')}"
837849
)
838850
if response.is_success:
851+
payload = self._decode_json(response)
852+
self._log_demo_restriction_messages(payload, request_label)
839853
if self._logger.isEnabledFor(logging.DEBUG):
840854
self._logger.debug(
841855
"Response %s status=%s", request_label, response.status_code
842856
)
843-
return self._decode_json(response)
857+
return payload
844858

845859
message, error_payload = self._extract_error_details(response)
846860
retry_after = _parse_retry_after_header(response.headers.get("Retry-After"))
@@ -888,6 +902,30 @@ def _decode_json(self, response: httpx.Response) -> Any:
888902
response_content=response.text,
889903
) from exc
890904

905+
def _log_demo_restriction_messages(self, payload: Any, request_label: str) -> None:
906+
if not isinstance(payload, Mapping):
907+
return
908+
909+
typed_payload = cast(Mapping[str, Any], payload)
910+
emitted_messages: set[str] = set()
911+
for field_name in _DEMO_RESTRICTION_MESSAGE_FIELDS:
912+
value = typed_payload.get(field_name)
913+
if not isinstance(value, str):
914+
continue
915+
message = value.strip()
916+
if not _is_demo_restriction_message(message):
917+
continue
918+
normalized_message = message.casefold()
919+
if normalized_message in emitted_messages:
920+
continue
921+
emitted_messages.add(normalized_message)
922+
self._logger.warning(
923+
"Demo mode restriction message in response %s field=%s: %s",
924+
request_label,
925+
field_name,
926+
message,
927+
)
928+
891929
@staticmethod
892930
def _extract_error_details(
893931
response: httpx.Response,

tests/test_client.py

Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525
VALID_API_KEY = "12345678-1234-1234-1234-123456789abc"
2626
ANOTHER_VALID_API_KEY = "abcdefab-cdef-abcd-efab-cdefabcdef12"
2727
ASYNC_API_KEY = "fedcba98-7654-3210-fedc-ba9876543210"
28+
DEMO_RESTRICTION_MESSAGE = (
29+
"Output has been watermarked or redacted. This API request was processed "
30+
"with a free account. Visit https://pdfrest.com/pricing/ to upgrade your "
31+
"plan and receive outputs without watermarks or redactions."
32+
)
2833

2934

3035
def _build_up_response() -> dict[str, Any]:
@@ -709,6 +714,107 @@ def handler(_: httpx.Request) -> httpx.Response:
709714
assert exc.value.response_content == "not-json"
710715

711716

717+
def test_client_logs_demo_restriction_message_warning(
718+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
719+
) -> None:
720+
monkeypatch.setenv("PDFREST_API_KEY", VALID_API_KEY)
721+
caplog.set_level("WARNING", logger="pdfrest.client")
722+
723+
def handler(_: httpx.Request) -> httpx.Response:
724+
return httpx.Response(
725+
200,
726+
json={**_build_up_response(), "message": DEMO_RESTRICTION_MESSAGE},
727+
)
728+
729+
transport = httpx.MockTransport(handler)
730+
with PdfRestClient(transport=transport) as client:
731+
_ = client.up()
732+
733+
assert "Demo mode restriction message in response" in caplog.text
734+
assert "field=message" in caplog.text
735+
assert DEMO_RESTRICTION_MESSAGE in caplog.text
736+
737+
738+
@pytest.mark.parametrize(
739+
("field_name", "body_value"),
740+
[
741+
pytest.param("message", DEMO_RESTRICTION_MESSAGE, id="message"),
742+
pytest.param("warning", DEMO_RESTRICTION_MESSAGE, id="warning"),
743+
pytest.param("keyMessage", DEMO_RESTRICTION_MESSAGE, id="key-message"),
744+
],
745+
)
746+
def test_client_logs_demo_restriction_message_warning_all_fields(
747+
monkeypatch: pytest.MonkeyPatch,
748+
caplog: pytest.LogCaptureFixture,
749+
field_name: str,
750+
body_value: str,
751+
) -> None:
752+
monkeypatch.setenv("PDFREST_API_KEY", VALID_API_KEY)
753+
caplog.set_level("WARNING", logger="pdfrest.client")
754+
755+
def handler(_: httpx.Request) -> httpx.Response:
756+
return httpx.Response(
757+
200,
758+
json={**_build_up_response(), field_name: body_value},
759+
)
760+
761+
transport = httpx.MockTransport(handler)
762+
with PdfRestClient(transport=transport) as client:
763+
_ = client.up()
764+
765+
assert "Demo mode restriction message in response" in caplog.text
766+
assert f"field={field_name}" in caplog.text
767+
assert DEMO_RESTRICTION_MESSAGE in caplog.text
768+
769+
770+
def test_client_logs_demo_restriction_message_once_when_duplicated(
771+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
772+
) -> None:
773+
monkeypatch.setenv("PDFREST_API_KEY", VALID_API_KEY)
774+
caplog.set_level("WARNING", logger="pdfrest.client")
775+
776+
def handler(_: httpx.Request) -> httpx.Response:
777+
return httpx.Response(
778+
200,
779+
json={
780+
**_build_up_response(),
781+
"message": DEMO_RESTRICTION_MESSAGE,
782+
"warning": DEMO_RESTRICTION_MESSAGE,
783+
"keyMessage": DEMO_RESTRICTION_MESSAGE,
784+
},
785+
)
786+
787+
transport = httpx.MockTransport(handler)
788+
with PdfRestClient(transport=transport) as client:
789+
_ = client.up()
790+
791+
demo_logs = [
792+
record.message
793+
for record in caplog.records
794+
if "Demo mode restriction message in response" in record.message
795+
]
796+
assert len(demo_logs) == 1
797+
798+
799+
def test_client_does_not_log_non_demo_key_message_warning(
800+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
801+
) -> None:
802+
monkeypatch.setenv("PDFREST_API_KEY", VALID_API_KEY)
803+
caplog.set_level("WARNING", logger="pdfrest.client")
804+
805+
def handler(_: httpx.Request) -> httpx.Response:
806+
return httpx.Response(
807+
200,
808+
json={**_build_up_response(), "keyMessage": "This is a test key"},
809+
)
810+
811+
transport = httpx.MockTransport(handler)
812+
with PdfRestClient(transport=transport) as client:
813+
_ = client.up()
814+
815+
assert "Demo mode restriction message in response" not in caplog.text
816+
817+
712818
@pytest.mark.asyncio
713819
async def test_async_client_raises_for_non_json_success_response(
714820
monkeypatch: pytest.MonkeyPatch,
@@ -728,6 +834,61 @@ def handler(_: httpx.Request) -> httpx.Response:
728834
assert exc.value.response_content == "not-json"
729835

730836

837+
@pytest.mark.asyncio
838+
async def test_async_client_logs_demo_restriction_message_warning(
839+
monkeypatch: pytest.MonkeyPatch, caplog: pytest.LogCaptureFixture
840+
) -> None:
841+
monkeypatch.setenv("PDFREST_API_KEY", ASYNC_API_KEY)
842+
caplog.set_level("WARNING", logger="pdfrest.client")
843+
844+
def handler(_: httpx.Request) -> httpx.Response:
845+
return httpx.Response(
846+
200,
847+
json={**_build_up_response(), "message": DEMO_RESTRICTION_MESSAGE},
848+
)
849+
850+
transport = httpx.MockTransport(handler)
851+
async with AsyncPdfRestClient(transport=transport) as client:
852+
_ = await client.up()
853+
854+
assert "Demo mode restriction message in response" in caplog.text
855+
assert "field=message" in caplog.text
856+
assert DEMO_RESTRICTION_MESSAGE in caplog.text
857+
858+
859+
@pytest.mark.asyncio
860+
@pytest.mark.parametrize(
861+
("field_name", "body_value"),
862+
[
863+
pytest.param("message", DEMO_RESTRICTION_MESSAGE, id="message"),
864+
pytest.param("warning", DEMO_RESTRICTION_MESSAGE, id="warning"),
865+
pytest.param("keyMessage", DEMO_RESTRICTION_MESSAGE, id="key-message"),
866+
],
867+
)
868+
async def test_async_client_logs_demo_restriction_message_warning_all_fields(
869+
monkeypatch: pytest.MonkeyPatch,
870+
caplog: pytest.LogCaptureFixture,
871+
field_name: str,
872+
body_value: str,
873+
) -> None:
874+
monkeypatch.setenv("PDFREST_API_KEY", ASYNC_API_KEY)
875+
caplog.set_level("WARNING", logger="pdfrest.client")
876+
877+
def handler(_: httpx.Request) -> httpx.Response:
878+
return httpx.Response(
879+
200,
880+
json={**_build_up_response(), field_name: body_value},
881+
)
882+
883+
transport = httpx.MockTransport(handler)
884+
async with AsyncPdfRestClient(transport=transport) as client:
885+
_ = await client.up()
886+
887+
assert "Demo mode restriction message in response" in caplog.text
888+
assert f"field={field_name}" in caplog.text
889+
assert DEMO_RESTRICTION_MESSAGE in caplog.text
890+
891+
731892
def test_client_uses_text_for_non_json_error_payload(
732893
monkeypatch: pytest.MonkeyPatch,
733894
) -> None:

0 commit comments

Comments
 (0)