Skip to content

Commit 0ae3779

Browse files
[7.13] Add support for the HTTP API compatibility header
Co-authored-by: Seth Michael Larson <[email protected]>
1 parent 93515dc commit 0ae3779

File tree

9 files changed

+84
-18
lines changed

9 files changed

+84
-18
lines changed

Diff for: docs/sphinx/connection.rst

+23-1
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,29 @@ To create an `SSLContext` object you only need to use one of cafile, capath or c
6060
* `capath` is the directory of a collection of CA's
6161
* `cadata` is either an ASCII string of one or more PEM-encoded certificates or a bytes-like object of DER-encoded certificates.
6262

63-
Please note that the use of SSLContext is only available for Urllib3.
63+
Please note that the use of SSLContext is only available for urllib3.
6464

6565
.. autoclass:: Urllib3HttpConnection
6666
:members:
67+
68+
69+
API Compatibility HTTP Header
70+
-----------------------------
71+
72+
The Python client can be configured to emit an HTTP header
73+
``Accept: application/vnd.elasticsearch+json; compatible-with=7``
74+
which signals to Elasticsearch that the client is requesting
75+
``7.x`` version of request and response bodies. This allows for
76+
upgrading from 7.x to 8.x version of Elasticsearch without upgrading
77+
everything at once. Elasticsearch should be upgraded first after
78+
the compatibility header is configured and clients should be upgraded
79+
second.
80+
81+
.. code-block:: python
82+
83+
from elasticsearch import Elasticsearch
84+
85+
client = Elasticsearch("http://...", headers={"accept": "application/vnd.elasticsearch+json; compatible-with=7"})
86+
87+
If you'd like to have the client emit the header without configuring ``headers`` you
88+
can use the environment variable ``ELASTIC_CLIENT_APIVERSIONING=1``.

Diff for: elasticsearch/connection/base.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import gzip
2020
import io
2121
import logging
22+
import os
2223
import re
2324
import warnings
2425
from platform import python_version
@@ -28,7 +29,7 @@
2829
except ImportError:
2930
import json
3031

31-
from .. import __versionstr__
32+
from .. import __version__, __versionstr__
3233
from ..exceptions import (
3334
HTTP_EXCEPTIONS,
3435
ElasticsearchWarning,
@@ -121,6 +122,13 @@ def __init__(
121122
if opaque_id:
122123
self.headers["x-opaque-id"] = opaque_id
123124

125+
if os.getenv("ELASTIC_CLIENT_APIVERSIONING") == "1":
126+
self.headers.setdefault(
127+
"accept",
128+
"application/vnd.elasticsearch+json;compatible-with=%s"
129+
% (str(__version__[0]),),
130+
)
131+
124132
self.headers.setdefault("content-type", "application/json")
125133
self.headers.setdefault("user-agent", self._get_default_user_agent())
126134

@@ -248,7 +256,7 @@ def perform_request(
248256
def log_request_success(
249257
self, method, full_url, path, body, status_code, response, duration
250258
):
251-
""" Log a successful API call. """
259+
"""Log a successful API call."""
252260
# TODO: optionally pass in params instead of full_url and do urlencode only when needed
253261

254262
# body has already been serialized to utf-8, deserialize it for logging
@@ -278,7 +286,7 @@ def log_request_fail(
278286
response=None,
279287
exception=None,
280288
):
281-
""" Log an unsuccessful API call. """
289+
"""Log an unsuccessful API call."""
282290
# do not log 404s on HEAD requests
283291
if method == "HEAD" and status_code == 404:
284292
return
@@ -307,7 +315,7 @@ def log_request_fail(
307315
logger.debug("< %s", response)
308316

309317
def _raise_error(self, status_code, raw_data):
310-
""" Locate appropriate exception and raise it. """
318+
"""Locate appropriate exception and raise it."""
311319
error_message = raw_data
312320
additional_info = None
313321
try:

Diff for: elasticsearch/exceptions.py

+8-8
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def status_code(self):
6868

6969
@property
7070
def error(self):
71-
""" A string error message. """
71+
"""A string error message."""
7272
return self.args[1]
7373

7474
@property
@@ -120,11 +120,11 @@ def __str__(self):
120120

121121

122122
class SSLError(ConnectionError):
123-
""" Error raised when encountering SSL errors. """
123+
"""Error raised when encountering SSL errors."""
124124

125125

126126
class ConnectionTimeout(ConnectionError):
127-
""" A network timeout. Doesn't cause a node retry by default. """
127+
"""A network timeout. Doesn't cause a node retry by default."""
128128

129129
def __str__(self):
130130
return "ConnectionTimeout caused by - %s(%s)" % (
@@ -134,23 +134,23 @@ def __str__(self):
134134

135135

136136
class NotFoundError(TransportError):
137-
""" Exception representing a 404 status code. """
137+
"""Exception representing a 404 status code."""
138138

139139

140140
class ConflictError(TransportError):
141-
""" Exception representing a 409 status code. """
141+
"""Exception representing a 409 status code."""
142142

143143

144144
class RequestError(TransportError):
145-
""" Exception representing a 400 status code. """
145+
"""Exception representing a 400 status code."""
146146

147147

148148
class AuthenticationException(TransportError):
149-
""" Exception representing a 401 status code. """
149+
"""Exception representing a 401 status code."""
150150

151151

152152
class AuthorizationException(TransportError):
153-
""" Exception representing a 403 status code. """
153+
"""Exception representing a 403 status code."""
154154

155155

156156
class ElasticsearchWarning(Warning):

Diff for: elasticsearch/helpers/errors.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
class BulkIndexError(ElasticsearchException):
2222
@property
2323
def errors(self):
24-
""" List of errors from execution of the last chunk. """
24+
"""List of errors from execution of the last chunk."""
2525
return self.args[1]
2626

2727

Diff for: elasticsearch/serializer.py

+6-2
Original file line numberDiff line numberDiff line change
@@ -154,8 +154,12 @@ def loads(self, s, mimetype=None):
154154
if not mimetype:
155155
deserializer = self.default
156156
else:
157-
# split out charset
158-
mimetype, _, _ = mimetype.partition(";")
157+
# split out 'charset' and 'compatible-width' options
158+
mimetype = mimetype.partition(";")[0].strip()
159+
# Treat 'application/vnd.elasticsearch+json'
160+
# as application/json for compatibility.
161+
if mimetype == "application/vnd.elasticsearch+json":
162+
mimetype = "application/json"
159163
try:
160164
deserializer = self.serializers[mimetype]
161165
except KeyError:

Diff for: test_elasticsearch/test_async/test_server/test_rest_api_spec.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ async def run(self):
7575
await self.teardown()
7676

7777
async def run_code(self, test):
78-
""" Execute an instruction based on it's type. """
78+
"""Execute an instruction based on it's type."""
7979
print(test)
8080
for action in test:
8181
assert len(action) == 1

Diff for: test_elasticsearch/test_connection.py

+21
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
import gzip
2020
import io
21+
import os
2122
import re
2223
import ssl
2324
import warnings
@@ -171,6 +172,26 @@ def test_meta_header(self):
171172
Connection(meta_header=1)
172173
assert str(e.value) == "meta_header must be of type bool"
173174

175+
def test_compatibility_accept_header(self):
176+
try:
177+
conn = Connection()
178+
assert "accept" not in conn.headers
179+
180+
os.environ["ELASTIC_CLIENT_APIVERSIONING"] = "0"
181+
182+
conn = Connection()
183+
assert "accept" not in conn.headers
184+
185+
os.environ["ELASTIC_CLIENT_APIVERSIONING"] = "1"
186+
187+
conn = Connection()
188+
assert (
189+
conn.headers["accept"]
190+
== "application/vnd.elasticsearch+json;compatible-with=8"
191+
)
192+
finally:
193+
os.environ.pop("ELASTIC_CLIENT_APIVERSIONING")
194+
174195

175196
class TestUrllib3Connection(TestCase):
176197
def _get_mock_connection(self, connection_params={}, response_body=b"{}"):

Diff for: test_elasticsearch/test_serializer.py

+11
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,17 @@ def test_deserializes_text_with_correct_ct(self):
185185
self.de.loads('{"some":"data"}', "text/plain; charset=whatever"),
186186
)
187187

188+
def test_deserialize_compatibility_header(self):
189+
for content_type in (
190+
"application/vnd.elasticsearch+json;compatible-with=7",
191+
"application/vnd.elasticsearch+json; compatible-with=7",
192+
"application/vnd.elasticsearch+json;compatible-with=8",
193+
"application/vnd.elasticsearch+json; compatible-with=8",
194+
):
195+
self.assertEqual(
196+
{"some": "data"}, self.de.loads('{"some":"data"}', content_type)
197+
)
198+
188199
def test_raises_serialization_error_on_unknown_mimetype(self):
189200
self.assertRaises(SerializationError, self.de.loads, "{}", "text/html")
190201

Diff for: test_elasticsearch/test_server/test_rest_api_spec.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def run(self):
138138
self.teardown()
139139

140140
def run_code(self, test):
141-
""" Execute an instruction based on it's type. """
141+
"""Execute an instruction based on it's type."""
142142
print(test)
143143
for action in test:
144144
assert len(action) == 1

0 commit comments

Comments
 (0)