Skip to content

Commit a7091dc

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 87c843a commit a7091dc

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
@@ -186,6 +186,7 @@
186186
BACKOFF_JITTER_SECONDS = 0.1
187187
RETRYABLE_STATUS_CODES = {408, 425, 429, 499}
188188
_SUCCESSFUL_DELETION_MESSAGE = "successfully deleted"
189+
_DEMO_RESTRICTION_MESSAGE_FIELDS = ("message", "warning", "keyMessage")
189190

190191

191192
HttpMethod = Literal["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD"]
@@ -257,6 +258,17 @@ def _parse_retry_after_header(header_value: str | None) -> float | None:
257258
return seconds if seconds > 0 else 0.0
258259

259260

261+
def _is_demo_restriction_message(value: str) -> bool:
262+
normalized = value.strip().casefold()
263+
if not normalized:
264+
return False
265+
return (
266+
"watermarked or redacted" in normalized
267+
and "free account" in normalized
268+
and "upgrade your plan" in normalized
269+
)
270+
271+
260272
FileContent = IO[bytes] | bytes | str
261273
FileTuple2 = tuple[str | None, FileContent]
262274
FileTuple3 = tuple[str | None, FileContent, str | None]
@@ -831,11 +843,13 @@ def _handle_response(self, response: httpx.Response) -> Any:
831843
f"{getattr(request, 'method', 'UNKNOWN')} {getattr(request, 'url', '')}"
832844
)
833845
if response.is_success:
846+
payload = self._decode_json(response)
847+
self._log_demo_restriction_messages(payload, request_label)
834848
if self._logger.isEnabledFor(logging.DEBUG):
835849
self._logger.debug(
836850
"Response %s status=%s", request_label, response.status_code
837851
)
838-
return self._decode_json(response)
852+
return payload
839853

840854
message, error_payload = self._extract_error_details(response)
841855
retry_after = _parse_retry_after_header(response.headers.get("Retry-After"))
@@ -883,6 +897,30 @@ def _decode_json(self, response: httpx.Response) -> Any:
883897
response_content=response.text,
884898
) from exc
885899

900+
def _log_demo_restriction_messages(self, payload: Any, request_label: str) -> None:
901+
if not isinstance(payload, Mapping):
902+
return
903+
904+
typed_payload = cast(Mapping[str, Any], payload)
905+
emitted_messages: set[str] = set()
906+
for field_name in _DEMO_RESTRICTION_MESSAGE_FIELDS:
907+
value = typed_payload.get(field_name)
908+
if not isinstance(value, str):
909+
continue
910+
message = value.strip()
911+
if not _is_demo_restriction_message(message):
912+
continue
913+
normalized_message = message.casefold()
914+
if normalized_message in emitted_messages:
915+
continue
916+
emitted_messages.add(normalized_message)
917+
self._logger.warning(
918+
"Demo mode restriction message in response %s field=%s: %s",
919+
request_label,
920+
field_name,
921+
message,
922+
)
923+
886924
@staticmethod
887925
def _extract_error_details(
888926
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)