Skip to content

Commit d218703

Browse files
committed
Addresses comment about centralising raise_kibana_error
1 parent 1d4b334 commit d218703

6 files changed

Lines changed: 120 additions & 62 deletions

File tree

src/elastic_evals/agent_builder/client.py

Lines changed: 8 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,8 @@
2929
UpdateAgentRequest,
3030
UpdateToolRequest,
3131
)
32-
from elastic_evals.api.response import parse_error_body
33-
from elastic_evals.api.retry import is_retryable_status_code, retry_kibana_api_call
32+
from elastic_evals.api.response import raise_kibana_error
33+
from elastic_evals.api.retry import retry_kibana_api_call
3434
from elastic_evals.utils.logging import log
3535

3636
logger = log.getChild(__name__)
@@ -315,19 +315,12 @@ async def call_converse(
315315
)
316316

317317
def _raise_error(self, response: httpx.Response, *, context: str) -> NoReturn:
318-
status_code = response.status_code
319-
body, body_text = parse_error_body(response)
320-
321-
message = f"Agent Builder request failed ({context}) with {status_code}"
322-
if body_text:
323-
message = f"{message}: {body_text}"
324-
325-
logger.error(message)
326-
raise AgentBuilderError(
327-
message=message,
328-
status_code=status_code,
329-
body=body,
330-
retryable=is_retryable_status_code(status_code),
318+
raise_kibana_error(
319+
response,
320+
error_cls=AgentBuilderError,
321+
context="Agent Builder request",
322+
operation=context,
323+
logger=logger,
331324
)
332325

333326
def _parse_success_body(self, response: httpx.Response, *, context: str) -> Any:

src/elastic_evals/api/datasets_client.py

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@
77
from __future__ import annotations
88

99
import uuid
10-
from typing import NoReturn
1110

1211
import httpx
1312

@@ -23,8 +22,8 @@
2322
)
2423
from elastic_evals.api.errors import DatasetSyncError
2524
from elastic_evals.api.headers import build_kibana_headers
26-
from elastic_evals.api.response import parse_error_body
27-
from elastic_evals.api.retry import is_retryable_status_code, retry_kibana_api_call
25+
from elastic_evals.api.response import raise_kibana_error
26+
from elastic_evals.api.retry import retry_kibana_api_call
2827

2928

3029
def compute_dataset_id(name: str) -> str:
@@ -65,7 +64,11 @@ async def upsert(
6564
if response.status_code == 200:
6665
return UpsertDatasetResponse.model_validate(response.json())
6766

68-
self._raise_dataset_sync_error(response)
67+
raise_kibana_error(
68+
response,
69+
error_cls=DatasetSyncError,
70+
context="Kibana dataset sync request",
71+
)
6972

7073
@retry_kibana_api_call
7174
async def get(self, dataset_id: str) -> GetDatasetResponse:
@@ -85,19 +88,8 @@ async def get(self, dataset_id: str) -> GetDatasetResponse:
8588
retryable=False,
8689
)
8790

88-
self._raise_dataset_sync_error(response)
89-
90-
def _raise_dataset_sync_error(self, response: httpx.Response) -> NoReturn:
91-
status_code = response.status_code
92-
body, body_text = parse_error_body(response)
93-
94-
message = f"Kibana dataset sync request failed with {status_code}"
95-
if body_text:
96-
message = f"{message}: {body_text}"
97-
98-
raise DatasetSyncError(
99-
message=message,
100-
status_code=status_code,
101-
body=body,
102-
retryable=is_retryable_status_code(status_code),
91+
raise_kibana_error(
92+
response,
93+
error_cls=DatasetSyncError,
94+
context="Kibana dataset sync request",
10395
)

src/elastic_evals/api/evaluators_client.py

Lines changed: 6 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@
2727
ValidateEvaluatorsResponse,
2828
)
2929
from elastic_evals.api.headers import build_kibana_headers
30-
from elastic_evals.api.response import parse_error_body
31-
from elastic_evals.api.retry import is_retryable_status_code, retry_kibana_api_call
30+
from elastic_evals.api.response import raise_kibana_error
31+
from elastic_evals.api.retry import retry_kibana_api_call
3232

3333

3434
class KibanaEvaluatorsClient:
@@ -95,15 +95,8 @@ async def _request(
9595
if response.status_code == 200:
9696
return response
9797

98-
status_code = response.status_code
99-
body, body_text = parse_error_body(response)
100-
message = f"Kibana evaluators request failed with {status_code}"
101-
if body_text:
102-
message = f"{message}: {body_text}"
103-
104-
raise KibanaEvaluatorsError(
105-
message=message,
106-
status_code=status_code,
107-
body=body,
108-
retryable=is_retryable_status_code(status_code),
98+
raise_kibana_error(
99+
response,
100+
error_cls=KibanaEvaluatorsError,
101+
context="Kibana evaluators request",
109102
)

src/elastic_evals/api/response.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,14 @@
77
from __future__ import annotations
88

99
import json
10-
from typing import Any
10+
import logging
11+
from typing import Any, NoReturn
1112

1213
import httpx
1314

15+
from elastic_evals.api.errors import KibanaAPIError
16+
from elastic_evals.api.retry import is_retryable_status_code
17+
1418

1519
def parse_error_body(response: httpx.Response) -> tuple[Any, str]:
1620
"""Parse an error body while preserving non-JSON response text."""
@@ -19,3 +23,29 @@ def parse_error_body(response: httpx.Response) -> tuple[Any, str]:
1923
return body, json.dumps(body, ensure_ascii=True)
2024
except (ValueError, json.JSONDecodeError):
2125
return response.text, response.text
26+
27+
28+
def raise_kibana_error(
29+
response: httpx.Response,
30+
*,
31+
error_cls: type[KibanaAPIError],
32+
context: str,
33+
operation: str | None = None,
34+
logger: logging.Logger | None = None,
35+
) -> NoReturn:
36+
"""Raise a typed Kibana API error from an unsuccessful response."""
37+
body, body_text = parse_error_body(response)
38+
message = f"{context} failed"
39+
if operation is not None:
40+
message = f"{message} ({operation})"
41+
message = f"{message} with {response.status_code}"
42+
if body_text:
43+
message = f"{message}: {body_text}"
44+
if logger is not None:
45+
logger.error(message)
46+
raise error_cls(
47+
message=message,
48+
status_code=response.status_code,
49+
body=body,
50+
retryable=is_retryable_status_code(response.status_code),
51+
)

src/elastic_evals/api/scores_client.py

Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@
1111
from elastic_evals.api.constants import EVALS_SCORES_URL
1212
from elastic_evals.api.errors import IngestScoresError
1313
from elastic_evals.api.headers import build_kibana_headers
14-
from elastic_evals.api.response import parse_error_body
15-
from elastic_evals.api.retry import is_retryable_status_code, retry_kibana_api_call
14+
from elastic_evals.api.response import raise_kibana_error
15+
from elastic_evals.api.retry import retry_kibana_api_call
1616
from elastic_evals.api.scores_models import IngestScoresRequest, IngestScoresResponse
1717
from elastic_evals.utils.logging import log
1818

@@ -54,16 +54,8 @@ async def ingest_scores(self, payload: IngestScoresRequest) -> IngestScoresRespo
5454
)
5555
return parsed
5656

57-
status_code = response.status_code
58-
body, body_text = parse_error_body(response)
59-
60-
message = f"Kibana score ingest request failed with {status_code}"
61-
if body_text:
62-
message = f"{message}: {body_text}"
63-
64-
raise IngestScoresError(
65-
message=message,
66-
status_code=status_code,
67-
body=body,
68-
retryable=is_retryable_status_code(status_code),
57+
raise_kibana_error(
58+
response,
59+
error_cls=IngestScoresError,
60+
context="Kibana score ingest request",
6961
)

tests/api/test_response.py

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
2+
# or more contributor license agreements. Licensed under the Elastic License 2.0;
3+
# you may not use this file except in compliance with the Elastic License 2.0.
4+
5+
import logging
6+
7+
import httpx
8+
import pytest
9+
10+
from elastic_evals.api.errors import KibanaEvaluatorsError
11+
from elastic_evals.api.response import raise_kibana_error
12+
13+
14+
def test_raise_kibana_error_parses_json_and_logs(
15+
caplog: pytest.LogCaptureFixture,
16+
) -> None:
17+
logger = logging.getLogger(__name__)
18+
19+
with pytest.raises(KibanaEvaluatorsError) as exc_info:
20+
raise_kibana_error(
21+
httpx.Response(503, json={"message": "temporarily unavailable"}),
22+
error_cls=KibanaEvaluatorsError,
23+
context="Kibana evaluators request",
24+
logger=logger,
25+
)
26+
27+
assert exc_info.value.message == (
28+
'Kibana evaluators request failed with 503: {"message": "temporarily unavailable"}'
29+
)
30+
assert exc_info.value.status_code == 503
31+
assert exc_info.value.body == {"message": "temporarily unavailable"}
32+
assert exc_info.value.retryable is True
33+
assert exc_info.value.message in caplog.text
34+
35+
36+
def test_raise_kibana_error_preserves_text_body() -> None:
37+
with pytest.raises(KibanaEvaluatorsError) as exc_info:
38+
raise_kibana_error(
39+
httpx.Response(400, text="invalid request"),
40+
error_cls=KibanaEvaluatorsError,
41+
context="Kibana evaluators request",
42+
)
43+
44+
assert exc_info.value.message == "Kibana evaluators request failed with 400: invalid request"
45+
assert exc_info.value.body == "invalid request"
46+
assert exc_info.value.retryable is False
47+
48+
49+
def test_raise_kibana_error_includes_operation() -> None:
50+
with pytest.raises(KibanaEvaluatorsError) as exc_info:
51+
raise_kibana_error(
52+
httpx.Response(400, text="invalid request"),
53+
error_cls=KibanaEvaluatorsError,
54+
context="Kibana evaluators request",
55+
operation="evaluate",
56+
)
57+
58+
assert exc_info.value.message == "Kibana evaluators request failed (evaluate) with 400: invalid request"

0 commit comments

Comments
 (0)