Skip to content

Commit f8bc21e

Browse files
Add back handling of the 'Warning' HTTP header
Co-authored-by: Seth Michael Larson <seth.larson@elastic.co>
1 parent c0d9bd1 commit f8bc21e

File tree

7 files changed

+162
-33
lines changed

7 files changed

+162
-33
lines changed

elasticsearch/_async/client/_base.py

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,27 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
from typing import Any, Collection, Mapping, Optional, Tuple, TypeVar, Union
18+
import re
19+
import warnings
20+
from typing import Any, Collection, Iterable, Mapping, Optional, Tuple, TypeVar, Union
1921

2022
from elastic_transport import AsyncTransport, HttpHeaders
2123
from elastic_transport.client_utils import DEFAULT, DefaultType, resolve_default
2224

23-
from ...compat import urlencode
24-
from ...exceptions import HTTP_EXCEPTIONS, ApiError, UnsupportedProductError
25+
from ...compat import urlencode, warn_stacklevel
26+
from ...exceptions import (
27+
HTTP_EXCEPTIONS,
28+
ApiError,
29+
ElasticsearchWarning,
30+
UnsupportedProductError,
31+
)
2532
from .utils import _base64_auth_header
2633

2734
SelfType = TypeVar("SelfType", bound="BaseClient")
2835
SelfNamespacedType = TypeVar("SelfNamespacedType", bound="NamespacedClient")
2936

37+
_WARNING_RE = re.compile(r"\"([^\"]*)\"")
38+
3039

3140
def resolve_auth_headers(
3241
headers: Optional[Mapping[str, str]],
@@ -153,6 +162,19 @@ async def _perform_request(
153162
body=response,
154163
)
155164

165+
# 'Warning' headers should be reraised as 'ElasticsearchWarning'
166+
warning_header = (meta.headers.get("warning") or "").strip()
167+
if warning_header:
168+
warning_messages: Iterable[str] = _WARNING_RE.findall(warning_header) or (
169+
warning_header,
170+
)
171+
for warning_message in warning_messages:
172+
warnings.warn(
173+
warning_message,
174+
category=ElasticsearchWarning,
175+
stacklevel=warn_stacklevel(),
176+
)
177+
156178
return response
157179

158180
def options(

elasticsearch/_sync/client/_base.py

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,27 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18+
import re
19+
import warnings
1820
from typing import Any, Collection, Mapping, Optional, Tuple, TypeVar, Union
1921

2022
from elastic_transport import HttpHeaders, Transport
2123
from elastic_transport.client_utils import DEFAULT, DefaultType, resolve_default
2224

23-
from ...compat import urlencode
24-
from ...exceptions import HTTP_EXCEPTIONS, ApiError, UnsupportedProductError
25+
from ...compat import urlencode, warn_stacklevel
26+
from ...exceptions import (
27+
HTTP_EXCEPTIONS,
28+
ApiError,
29+
ElasticsearchWarning,
30+
UnsupportedProductError,
31+
)
2532
from .utils import _base64_auth_header
2633

2734
SelfType = TypeVar("SelfType", bound="BaseClient")
2835
SelfNamespacedType = TypeVar("SelfNamespacedType", bound="NamespacedClient")
2936

37+
_WARNING_RE = re.compile(r"\"([^\"]*)\"")
38+
3039

3140
def resolve_auth_headers(
3241
headers: Optional[Mapping[str, str]],
@@ -153,6 +162,18 @@ def _perform_request(
153162
body=response,
154163
)
155164

165+
# 'Warning' headers should be reraised as 'ElasticsearchWarning'
166+
warning_header = (meta.headers.get("warning") or "").strip()
167+
if warning_header:
168+
for warning_message in _WARNING_RE.findall(warning_header) or (
169+
warning_header,
170+
):
171+
warnings.warn(
172+
warning_message,
173+
category=ElasticsearchWarning,
174+
stacklevel=warn_stacklevel(),
175+
)
176+
156177
return response
157178

158179
def options(

elasticsearch/compat.py

Lines changed: 41 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,12 @@
1515
# specific language governing permissions and limitations
1616
# under the License.
1717

18-
from queue import Queue
18+
import inspect
19+
import sys
20+
from pathlib import Path
1921
from typing import Mapping, Tuple, Type, Union
20-
from urllib.parse import quote, quote_plus, unquote
22+
from urllib.parse import quote
2123
from urllib.parse import urlencode as _urlencode
22-
from urllib.parse import urlparse
2324

2425
from elastic_transport.client_utils import percent_encode
2526

@@ -42,30 +43,47 @@ def to_bytes(x: Union[str, bytes], encoding: str = "ascii") -> bytes:
4243
return x
4344

4445

45-
try:
46-
from collections.abc import Mapping
47-
except ImportError:
48-
from collections import Mapping
49-
50-
51-
reraise_exceptions: Tuple[Type[Exception], ...] = (RecursionError,)
52-
53-
try:
54-
import asyncio
55-
56-
reraise_exceptions += (asyncio.CancelledError,)
57-
except (ImportError, AttributeError):
58-
pass
46+
def warn_stacklevel() -> int:
47+
"""Dynamically determine warning stacklevel for warnings based on the call stack"""
48+
try:
49+
# Grab the root module from the current module '__name__'
50+
module_name = __name__.partition(".")[0]
51+
module_path = Path(sys.modules[module_name].__file__)
52+
53+
# If the module is a folder we're looking at
54+
# subdirectories, otherwise we're looking for
55+
# an exact match.
56+
module_is_folder = module_path.name == "__init__.py"
57+
if module_is_folder:
58+
module_path = module_path.parent
59+
60+
# Look through frames until we find a file that
61+
# isn't a part of our module, then return that stacklevel.
62+
for level, frame in enumerate(inspect.stack()):
63+
# Garbage collecting frames
64+
frame_filename = Path(frame.filename)
65+
del frame
66+
67+
if (
68+
# If the module is a folder we look at subdirectory
69+
module_is_folder
70+
and module_path not in frame_filename.parents
71+
) or (
72+
# Otherwise we're looking for an exact match.
73+
not module_is_folder
74+
and module_path != frame_filename
75+
):
76+
return level
77+
except KeyError:
78+
pass
79+
return 0
5980

6081

6182
__all__ = [
6283
"string_types",
63-
"reraise_exceptions",
64-
"quote_plus",
84+
"to_str",
85+
"to_bytes",
6586
"quote",
6687
"urlencode",
67-
"unquote",
68-
"urlparse",
69-
"Queue",
70-
"Mapping",
88+
"warn_stacklevel",
7189
]

elasticsearch/helpers/actions.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,10 @@
1818
import logging
1919
import time
2020
from operator import methodcaller
21+
from queue import Queue
22+
from typing import Mapping
2123

22-
from ..compat import Mapping, Queue, string_types, to_bytes
24+
from ..compat import string_types, to_bytes
2325
from ..exceptions import NotFoundError, TransportError
2426
from .errors import BulkIndexError, ScanError
2527

test_elasticsearch/test_async/test_transport.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import asyncio
2222
import json
2323
import re
24+
import warnings
2425

2526
import pytest
2627
from elastic_transport import ApiResponseMeta, BaseAsyncNode, HttpHeaders, NodeConfig
@@ -30,6 +31,7 @@
3031
from elasticsearch import AsyncElasticsearch
3132
from elasticsearch.exceptions import (
3233
ConnectionError,
34+
ElasticsearchWarning,
3335
TransportError,
3436
UnsupportedProductError,
3537
)
@@ -622,3 +624,34 @@ async def test_transport_error_raised_before_product_error(status):
622624
calls = client.transport.node_pool.get().calls
623625
assert len(calls) == 1
624626
assert calls[0][0] == ("GET", "/")
627+
628+
629+
@pytest.mark.parametrize(
630+
"headers",
631+
[
632+
{
633+
"Warning": '299 Elasticsearch-8.0.0-SNAPSHOT-ad975cacd240b3329e160673c432e768dcd7899a "[xpack.monitoring.history.duration] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version."',
634+
"X-elastic-product": "Elasticsearch",
635+
},
636+
{
637+
"Warning": '299 Elasticsearch-8.0.0-SNAPSHOT-ad975cacd240b3329e160673c432e768dcd7899a "[xpack.monitoring.history.duration] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version.", 299 Elasticsearch-8.0.0-SNAPSHOT-ad975cacd240b3329e160673c432e768dcd7899a "[xpack.monitoring.history.duration2] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version."',
638+
"X-elastic-product": "Elasticsearch",
639+
},
640+
],
641+
)
642+
async def test_warning_header(headers):
643+
client = AsyncElasticsearch(
644+
[NodeConfig("http", "localhost", 9200, _extras={"headers": headers})],
645+
meta_header=False,
646+
node_class=DummyNode,
647+
)
648+
649+
with warnings.catch_warnings(record=True) as w:
650+
await client.info()
651+
652+
assert len(w) == headers["Warning"].count("299")
653+
assert w[0].category == ElasticsearchWarning
654+
assert (
655+
str(w[0].message)
656+
== "[xpack.monitoring.history.duration] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version."
657+
)

test_elasticsearch/test_server/test_rest_api_spec.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -275,13 +275,13 @@ def run_catch(self, catch, exception):
275275

276276
assert isinstance(exception, ApiError)
277277
if catch in CATCH_CODES:
278-
assert CATCH_CODES[catch] == exception.status
278+
assert CATCH_CODES[catch] == exception.meta.status
279279
elif catch[0] == "/" and catch[-1] == "/":
280280
assert (
281-
re.search(catch[1:-1], str(exception.message)),
282-
f"{catch} not in {str(exception.message)!r}",
281+
re.search(catch[1:-1], str(exception.body)),
282+
f"{catch} not in {str(exception.body)!r}",
283283
) is not None
284-
self.last_response = exception.message
284+
self.last_response = exception.body
285285

286286
def run_skip(self, skip):
287287
global IMPLEMENTED_FEATURES

test_elasticsearch/test_transport.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import json
2222
import re
2323
import time
24+
import warnings
2425

2526
import pytest
2627
from elastic_transport import ApiResponseMeta, BaseNode, HttpHeaders, NodeConfig
@@ -30,6 +31,7 @@
3031
from elasticsearch import Elasticsearch
3132
from elasticsearch.exceptions import (
3233
ConnectionError,
34+
ElasticsearchWarning,
3335
TransportError,
3436
UnsupportedProductError,
3537
)
@@ -504,3 +506,34 @@ def test_transport_error_raised_before_product_error(status):
504506
calls = client.transport.node_pool.get().calls
505507
assert len(calls) == 1
506508
assert calls[0][0] == ("GET", "/")
509+
510+
511+
@pytest.mark.parametrize(
512+
"headers",
513+
[
514+
{
515+
"Warning": '299 Elasticsearch-8.0.0-SNAPSHOT-ad975cacd240b3329e160673c432e768dcd7899a "[xpack.monitoring.history.duration] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version."',
516+
"X-elastic-product": "Elasticsearch",
517+
},
518+
{
519+
"Warning": '299 Elasticsearch-8.0.0-SNAPSHOT-ad975cacd240b3329e160673c432e768dcd7899a "[xpack.monitoring.history.duration] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version.", 299 Elasticsearch-8.0.0-SNAPSHOT-ad975cacd240b3329e160673c432e768dcd7899a "[xpack.monitoring.history.duration2] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version."',
520+
"X-elastic-product": "Elasticsearch",
521+
},
522+
],
523+
)
524+
def test_warning_header(headers):
525+
client = Elasticsearch(
526+
[NodeConfig("http", "localhost", 9200, _extras={"headers": headers})],
527+
meta_header=False,
528+
node_class=DummyNode,
529+
)
530+
531+
with warnings.catch_warnings(record=True) as w:
532+
client.info()
533+
534+
assert len(w) == headers["Warning"].count("299")
535+
assert w[0].category == ElasticsearchWarning
536+
assert (
537+
str(w[0].message)
538+
== "[xpack.monitoring.history.duration] setting was deprecated in Elasticsearch and will be removed in a future release! See the breaking changes documentation for the next major version."
539+
)

0 commit comments

Comments
 (0)