cohere-parse / tests /test_parser_client.py
dennis-zhao's picture
Migrate to the Parse API
270d701 verified
Raw
History Blame Contribute Delete
7.92 kB
from __future__ import annotations
import base64
import pytest
import requests
from parser_client import (
ParserClient,
ParserConfig,
ParserServiceError,
api_key_name_for_environment,
build_payload,
selected_environment,
)
class FakeResponse:
def __init__(
self,
status: int,
body: dict,
headers: dict | None = None,
) -> None:
self.status_code = status
self._body = body
self.headers = headers or {}
self.ok = 200 <= status < 300
def json(self) -> dict:
return self._body
class FakeSession:
def __init__(self, responses: list[FakeResponse | Exception]) -> None:
self.responses = list(responses)
self.calls: list[dict] = []
def post(self, url: str, **kwargs):
self.calls.append({"url": url, **kwargs})
response = self.responses.pop(0)
if isinstance(response, Exception):
raise response
return response
def config() -> ParserConfig:
return ParserConfig(
api_url="https://stg.api.cohere.test/v2/parse",
api_key="secret-key",
model="parse-v5.0",
timeout=12,
max_retries=3,
)
def clear_cohere_environment(monkeypatch: pytest.MonkeyPatch) -> None:
for name in (
"COHERE_ENV",
"COHERE_API_KEY",
"COHERE_STG_API_KEY",
"COHERE_API_URL",
):
monkeypatch.delenv(name, raising=False)
def test_environment_defaults_to_staging(monkeypatch: pytest.MonkeyPatch) -> None:
clear_cohere_environment(monkeypatch)
monkeypatch.setenv("COHERE_STG_API_KEY", "staging-key")
parsed = ParserConfig.from_environment()
assert selected_environment() == "staging"
assert api_key_name_for_environment() == "COHERE_STG_API_KEY"
assert parsed.api_key == "staging-key"
assert parsed.api_url == "https://stg.api.cohere.ai/v2/parse"
def test_production_environment_uses_production_key_and_url(
monkeypatch: pytest.MonkeyPatch,
) -> None:
clear_cohere_environment(monkeypatch)
monkeypatch.setenv("COHERE_ENV", "production")
monkeypatch.setenv("COHERE_API_KEY", "production-key")
monkeypatch.setenv("COHERE_STG_API_KEY", "staging-key")
parsed = ParserConfig.from_environment()
assert api_key_name_for_environment() == "COHERE_API_KEY"
assert parsed.api_key == "production-key"
assert parsed.api_url == "https://api.cohere.com/v2/parse"
def test_api_url_can_override_the_selected_environment(
monkeypatch: pytest.MonkeyPatch,
) -> None:
clear_cohere_environment(monkeypatch)
monkeypatch.setenv("COHERE_STG_API_KEY", "staging-key")
monkeypatch.setenv("COHERE_API_URL", "https://gateway.example.test/v2/parse")
parsed = ParserConfig.from_environment()
assert parsed.api_url == "https://gateway.example.test/v2/parse"
def test_environment_rejects_invalid_values(monkeypatch: pytest.MonkeyPatch) -> None:
clear_cohere_environment(monkeypatch)
monkeypatch.setenv("COHERE_ENV", "preview")
with pytest.raises(ParserServiceError, match="staging.*production"):
ParserConfig.from_environment()
def test_environment_requires_its_matching_key(
monkeypatch: pytest.MonkeyPatch,
) -> None:
clear_cohere_environment(monkeypatch)
monkeypatch.setenv("COHERE_ENV", "production")
monkeypatch.setenv("COHERE_STG_API_KEY", "staging-key")
with pytest.raises(ParserServiceError, match="configured for production"):
ParserConfig.from_environment()
def test_build_payload_uses_image_only_contract() -> None:
payload = build_payload("parser-model", b"png-bytes")
assert payload == {
"model": "parser-model",
"document": {
"type": "image_url",
"image_url": payload["document"]["image_url"],
},
"output_format": "blocks",
}
data_url = payload["document"]["image_url"]
assert data_url.startswith("data:image/png;base64,")
assert base64.b64decode(data_url.split(",", 1)[1]) == b"png-bytes"
def parse_response(content: str = "# Invoice") -> dict:
return {
"id": "parse-response-123",
"pages": [
{
"index": 0,
"type": "blocks",
"blocks": [
{"type": "text", "text": {"content": content}},
{
"type": "table",
"table": {
"type": "html",
"html": "<table><tr><td>Total</td></tr></table>",
"bounding_box": {
"top_left_x": 10,
"top_left_y": 20,
"bottom_right_x": 200,
"bottom_right_y": 100,
},
"bounding_box_normalized": {
"top_left_x": 0.1,
"top_left_y": 0.2,
"bottom_right_x": 0.8,
"bottom_right_y": 0.6,
},
},
},
],
}
],
"meta": {"billed_units": {"pages": 1}},
}
def test_parse_page_calls_staging_and_shapes_output() -> None:
session = FakeSession(
[
FakeResponse(
200,
parse_response(),
{"x-request-id": "request-123"},
)
]
)
client = ParserClient(config(), session=session, sleeper=lambda _: None)
result = client.parse_page(b"page")
assert session.calls[0]["url"] == config().api_url
assert session.calls[0]["timeout"] == 12
assert session.calls[0]["headers"]["Authorization"] == "Bearer secret-key"
assert session.calls[0]["json"]["output_format"] == "blocks"
assert result["text_output"].startswith("# Invoice")
assert '"id": "parse-response-123"' in result["raw_output"]
assert result["boxes"][0]["label"] == "table"
assert result["usage"] == {"billed_units": {"pages": 1}}
assert result["request_id"] == "request-123"
assert result["model"] == config().model
def test_retries_retryable_responses() -> None:
session = FakeSession(
[
FakeResponse(503, {}, {"Retry-After": "0"}),
FakeResponse(200, parse_response("ok")),
]
)
delays = []
client = ParserClient(config(), session=session, sleeper=delays.append)
result = client.parse_page(b"page")
assert result["text_output"].startswith("ok")
assert len(session.calls) == 2
assert delays == [0.0]
def test_rejects_invalid_success_response() -> None:
session = FakeSession([FakeResponse(200, {"id": "missing-pages"})])
client = ParserClient(config(), session=session, sleeper=lambda _: None)
with pytest.raises(ParserServiceError, match="invalid response"):
client.parse_page(b"page")
def test_retries_transport_errors_then_returns_safe_error() -> None:
session = FakeSession(
[
requests.ConnectionError("private network details"),
requests.Timeout("private network details"),
requests.ConnectionError("private network details"),
]
)
client = ParserClient(config(), session=session, sleeper=lambda _: None)
with pytest.raises(ParserServiceError, match="could not be reached"):
client.parse_page(b"page")
def test_rejects_non_retryable_response_without_leaking_body() -> None:
session = FakeSession([FakeResponse(401, {"message": "secret detail"})])
client = ParserClient(config(), session=session, sleeper=lambda _: None)
with pytest.raises(ParserServiceError) as error:
client.parse_page(b"page")
assert "401" in str(error.value)
assert "secret detail" not in str(error.value)