crate 2.0.0.dev0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- crate/client/__init__.py +37 -0
- crate/client/_pep440.py +1 -0
- crate/client/blob.py +105 -0
- crate/client/connection.py +221 -0
- crate/client/converter.py +143 -0
- crate/client/cursor.py +321 -0
- crate/client/exceptions.py +101 -0
- crate/client/http.py +694 -0
- crate/testing/__init__.py +0 -0
- crate/testing/layer.py +428 -0
- crate/testing/util.py +95 -0
- crate-2.0.0.dev0.dist-info/LICENSE +178 -0
- crate-2.0.0.dev0.dist-info/METADATA +168 -0
- crate-2.0.0.dev0.dist-info/NOTICE +24 -0
- crate-2.0.0.dev0.dist-info/RECORD +17 -0
- crate-2.0.0.dev0.dist-info/WHEEL +5 -0
- crate-2.0.0.dev0.dist-info/top_level.txt +1 -0
crate/client/http.py
ADDED
@@ -0,0 +1,694 @@
|
|
1
|
+
# -*- coding: utf-8; -*-
|
2
|
+
#
|
3
|
+
# Licensed to CRATE Technology GmbH ("Crate") under one or more contributor
|
4
|
+
# license agreements. See the NOTICE file distributed with this work for
|
5
|
+
# additional information regarding copyright ownership. Crate licenses
|
6
|
+
# this file to you under the Apache License, Version 2.0 (the "License");
|
7
|
+
# you may not use this file except in compliance with the License. You may
|
8
|
+
# obtain a copy of the License at
|
9
|
+
#
|
10
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
11
|
+
#
|
12
|
+
# Unless required by applicable law or agreed to in writing, software
|
13
|
+
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
|
14
|
+
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
|
15
|
+
# License for the specific language governing permissions and limitations
|
16
|
+
# under the License.
|
17
|
+
#
|
18
|
+
# However, if you have executed another commercial license agreement
|
19
|
+
# with Crate these terms will supersede the license and you may use the
|
20
|
+
# software solely pursuant to the terms of the relevant commercial agreement.
|
21
|
+
|
22
|
+
|
23
|
+
import calendar
|
24
|
+
import heapq
|
25
|
+
import io
|
26
|
+
import logging
|
27
|
+
import os
|
28
|
+
import re
|
29
|
+
import socket
|
30
|
+
import ssl
|
31
|
+
import threading
|
32
|
+
from base64 import b64encode
|
33
|
+
from datetime import date, datetime, timezone
|
34
|
+
from decimal import Decimal
|
35
|
+
from time import time
|
36
|
+
from urllib.parse import urlparse
|
37
|
+
from uuid import UUID
|
38
|
+
|
39
|
+
import orjson
|
40
|
+
import urllib3
|
41
|
+
from urllib3 import connection_from_url
|
42
|
+
from urllib3.connection import HTTPConnection
|
43
|
+
from urllib3.exceptions import (
|
44
|
+
HTTPError,
|
45
|
+
MaxRetryError,
|
46
|
+
ProtocolError,
|
47
|
+
ProxyError,
|
48
|
+
ReadTimeoutError,
|
49
|
+
SSLError,
|
50
|
+
)
|
51
|
+
from urllib3.util.retry import Retry
|
52
|
+
from verlib2 import Version
|
53
|
+
|
54
|
+
from crate.client.exceptions import (
|
55
|
+
BlobLocationNotFoundException,
|
56
|
+
ConnectionError,
|
57
|
+
DigestNotFoundException,
|
58
|
+
IntegrityError,
|
59
|
+
ProgrammingError,
|
60
|
+
)
|
61
|
+
|
62
|
+
logger = logging.getLogger(__name__)
|
63
|
+
|
64
|
+
|
65
|
+
_HTTP_PAT = pat = re.compile("https?://.+", re.I)
|
66
|
+
SRV_UNAVAILABLE_STATUSES = {502, 503, 504, 509}
|
67
|
+
PRESERVE_ACTIVE_SERVER_EXCEPTIONS = {ConnectionResetError, BrokenPipeError}
|
68
|
+
SSL_ONLY_ARGS = {"ca_certs", "cert_reqs", "cert_file", "key_file"}
|
69
|
+
|
70
|
+
|
71
|
+
def super_len(o):
|
72
|
+
if hasattr(o, "__len__"):
|
73
|
+
return len(o)
|
74
|
+
if hasattr(o, "len"):
|
75
|
+
return o.len
|
76
|
+
if hasattr(o, "fileno"):
|
77
|
+
try:
|
78
|
+
fileno = o.fileno()
|
79
|
+
except io.UnsupportedOperation:
|
80
|
+
pass
|
81
|
+
else:
|
82
|
+
return os.fstat(fileno).st_size
|
83
|
+
if hasattr(o, "getvalue"):
|
84
|
+
# e.g. BytesIO, cStringIO.StringI
|
85
|
+
return len(o.getvalue())
|
86
|
+
return None
|
87
|
+
|
88
|
+
|
89
|
+
epoch_aware = datetime(1970, 1, 1, tzinfo=timezone.utc)
|
90
|
+
epoch_naive = datetime(1970, 1, 1)
|
91
|
+
|
92
|
+
|
93
|
+
def cratedb_json_encoder(obj):
|
94
|
+
"""
|
95
|
+
Encoder function for orjson.
|
96
|
+
|
97
|
+
https://github.com/ijl/orjson#default
|
98
|
+
https://github.com/ijl/orjson#opt_passthrough_datetime
|
99
|
+
"""
|
100
|
+
if isinstance(obj, (Decimal, UUID)):
|
101
|
+
return str(obj)
|
102
|
+
if isinstance(obj, datetime):
|
103
|
+
if obj.tzinfo is not None:
|
104
|
+
delta = obj - epoch_aware
|
105
|
+
else:
|
106
|
+
delta = obj - epoch_naive
|
107
|
+
return int(
|
108
|
+
delta.microseconds / 1000.0
|
109
|
+
+ (delta.seconds + delta.days * 24 * 3600) * 1000.0
|
110
|
+
)
|
111
|
+
if isinstance(obj, date):
|
112
|
+
return calendar.timegm(obj.timetuple()) * 1000
|
113
|
+
return obj
|
114
|
+
|
115
|
+
|
116
|
+
class Server:
|
117
|
+
def __init__(self, server, **pool_kw):
|
118
|
+
socket_options = _get_socket_opts(
|
119
|
+
pool_kw.pop("socket_keepalive", False),
|
120
|
+
pool_kw.pop("socket_tcp_keepidle", None),
|
121
|
+
pool_kw.pop("socket_tcp_keepintvl", None),
|
122
|
+
pool_kw.pop("socket_tcp_keepcnt", None),
|
123
|
+
)
|
124
|
+
self.pool = connection_from_url(
|
125
|
+
server,
|
126
|
+
socket_options=socket_options,
|
127
|
+
**pool_kw,
|
128
|
+
)
|
129
|
+
|
130
|
+
def request(
|
131
|
+
self,
|
132
|
+
method,
|
133
|
+
path,
|
134
|
+
data=None,
|
135
|
+
stream=False,
|
136
|
+
headers=None,
|
137
|
+
username=None,
|
138
|
+
password=None,
|
139
|
+
schema=None,
|
140
|
+
backoff_factor=0,
|
141
|
+
**kwargs,
|
142
|
+
):
|
143
|
+
"""Send a request
|
144
|
+
|
145
|
+
Always set the Content-Length and the Content-Type header.
|
146
|
+
"""
|
147
|
+
if headers is None:
|
148
|
+
headers = {}
|
149
|
+
if "Content-Length" not in headers:
|
150
|
+
length = super_len(data)
|
151
|
+
if length is not None:
|
152
|
+
headers["Content-Length"] = length
|
153
|
+
|
154
|
+
# Authentication credentials
|
155
|
+
if username is not None:
|
156
|
+
if "Authorization" not in headers and username is not None:
|
157
|
+
credentials = username + ":"
|
158
|
+
if password is not None:
|
159
|
+
credentials += password
|
160
|
+
headers["Authorization"] = "Basic %s" % b64encode(
|
161
|
+
credentials.encode("utf-8")
|
162
|
+
).decode("utf-8")
|
163
|
+
# For backwards compatibility with Crate <= 2.2
|
164
|
+
if "X-User" not in headers:
|
165
|
+
headers["X-User"] = username
|
166
|
+
|
167
|
+
if schema is not None:
|
168
|
+
headers["Default-Schema"] = schema
|
169
|
+
headers["Accept"] = "application/json"
|
170
|
+
headers["Content-Type"] = "application/json"
|
171
|
+
kwargs["assert_same_host"] = False
|
172
|
+
kwargs["redirect"] = False
|
173
|
+
kwargs["retries"] = Retry(read=0, backoff_factor=backoff_factor)
|
174
|
+
return self.pool.urlopen(
|
175
|
+
method,
|
176
|
+
path,
|
177
|
+
body=data,
|
178
|
+
preload_content=not stream,
|
179
|
+
headers=headers,
|
180
|
+
**kwargs,
|
181
|
+
)
|
182
|
+
|
183
|
+
def close(self):
|
184
|
+
self.pool.close()
|
185
|
+
|
186
|
+
|
187
|
+
def _json_from_response(response):
|
188
|
+
try:
|
189
|
+
return orjson.loads(response.data)
|
190
|
+
except ValueError as ex:
|
191
|
+
raise ProgrammingError(
|
192
|
+
"Invalid server response of content-type '{}':\n{}".format(
|
193
|
+
response.headers.get("content-type", "unknown"),
|
194
|
+
response.data.decode("utf-8"),
|
195
|
+
)
|
196
|
+
) from ex
|
197
|
+
|
198
|
+
|
199
|
+
def _blob_path(table, digest):
|
200
|
+
return "/_blobs/{table}/{digest}".format(table=table, digest=digest)
|
201
|
+
|
202
|
+
|
203
|
+
def _ex_to_message(ex):
|
204
|
+
return getattr(ex, "message", None) or str(ex) or repr(ex)
|
205
|
+
|
206
|
+
|
207
|
+
def _raise_for_status(response):
|
208
|
+
"""
|
209
|
+
Raise `IntegrityError` exceptions for `DuplicateKeyException` errors.
|
210
|
+
"""
|
211
|
+
try:
|
212
|
+
return _raise_for_status_real(response)
|
213
|
+
except ProgrammingError as ex:
|
214
|
+
if "DuplicateKeyException" in ex.message:
|
215
|
+
raise IntegrityError(ex.message, error_trace=ex.error_trace) from ex
|
216
|
+
raise
|
217
|
+
|
218
|
+
|
219
|
+
def _raise_for_status_real(response):
|
220
|
+
"""make sure that only crate.exceptions are raised that are defined in
|
221
|
+
the DB-API specification"""
|
222
|
+
message = ""
|
223
|
+
if 400 <= response.status < 500:
|
224
|
+
message = "%s Client Error: %s" % (response.status, response.reason)
|
225
|
+
elif 500 <= response.status < 600:
|
226
|
+
message = "%s Server Error: %s" % (response.status, response.reason)
|
227
|
+
else:
|
228
|
+
return
|
229
|
+
if response.status == 503:
|
230
|
+
raise ConnectionError(message)
|
231
|
+
if response.headers.get("content-type", "").startswith("application/json"):
|
232
|
+
data = orjson.loads(response.data)
|
233
|
+
error = data.get("error", {})
|
234
|
+
error_trace = data.get("error_trace", None)
|
235
|
+
if "results" in data:
|
236
|
+
errors = [
|
237
|
+
res["error_message"]
|
238
|
+
for res in data["results"]
|
239
|
+
if res.get("error_message")
|
240
|
+
]
|
241
|
+
if errors:
|
242
|
+
raise ProgrammingError("\n".join(errors))
|
243
|
+
if isinstance(error, dict):
|
244
|
+
raise ProgrammingError(
|
245
|
+
error.get("message", ""), error_trace=error_trace
|
246
|
+
)
|
247
|
+
raise ProgrammingError(error, error_trace=error_trace)
|
248
|
+
raise ProgrammingError(message)
|
249
|
+
|
250
|
+
|
251
|
+
def _server_url(server):
|
252
|
+
"""
|
253
|
+
Normalizes a given server string to an url
|
254
|
+
|
255
|
+
>>> print(_server_url('a'))
|
256
|
+
http://a
|
257
|
+
>>> print(_server_url('a:9345'))
|
258
|
+
http://a:9345
|
259
|
+
>>> print(_server_url('https://a:9345'))
|
260
|
+
https://a:9345
|
261
|
+
>>> print(_server_url('https://a'))
|
262
|
+
https://a
|
263
|
+
>>> print(_server_url('demo.crate.io'))
|
264
|
+
http://demo.crate.io
|
265
|
+
"""
|
266
|
+
if not _HTTP_PAT.match(server):
|
267
|
+
server = "http://%s" % server
|
268
|
+
parsed = urlparse(server)
|
269
|
+
url = "%s://%s" % (parsed.scheme, parsed.netloc)
|
270
|
+
return url
|
271
|
+
|
272
|
+
|
273
|
+
def _to_server_list(servers):
|
274
|
+
if isinstance(servers, str):
|
275
|
+
servers = servers.split()
|
276
|
+
return [_server_url(s) for s in servers]
|
277
|
+
|
278
|
+
|
279
|
+
def _pool_kw_args(
|
280
|
+
verify_ssl_cert,
|
281
|
+
ca_cert,
|
282
|
+
client_cert,
|
283
|
+
client_key,
|
284
|
+
timeout=None,
|
285
|
+
pool_size=None,
|
286
|
+
):
|
287
|
+
ca_cert = ca_cert or os.environ.get("REQUESTS_CA_BUNDLE", None)
|
288
|
+
if ca_cert and not os.path.exists(ca_cert):
|
289
|
+
# Sanity check
|
290
|
+
raise IOError('CA bundle file "{}" does not exist.'.format(ca_cert))
|
291
|
+
|
292
|
+
kw = {
|
293
|
+
"ca_certs": ca_cert,
|
294
|
+
"cert_reqs": ssl.CERT_REQUIRED if verify_ssl_cert else ssl.CERT_NONE,
|
295
|
+
"cert_file": client_cert,
|
296
|
+
"key_file": client_key,
|
297
|
+
}
|
298
|
+
if timeout is not None:
|
299
|
+
if isinstance(timeout, str):
|
300
|
+
timeout = float(timeout)
|
301
|
+
kw["timeout"] = timeout
|
302
|
+
if pool_size is not None:
|
303
|
+
kw["maxsize"] = int(pool_size)
|
304
|
+
return kw
|
305
|
+
|
306
|
+
|
307
|
+
def _remove_certs_for_non_https(server, kwargs):
|
308
|
+
if server.lower().startswith("https"):
|
309
|
+
return kwargs
|
310
|
+
used_ssl_args = SSL_ONLY_ARGS & set(kwargs.keys())
|
311
|
+
if used_ssl_args:
|
312
|
+
kwargs = kwargs.copy()
|
313
|
+
for arg in used_ssl_args:
|
314
|
+
kwargs.pop(arg)
|
315
|
+
return kwargs
|
316
|
+
|
317
|
+
|
318
|
+
def _update_pool_kwargs_for_ssl_minimum_version(server, kwargs):
|
319
|
+
"""
|
320
|
+
On urllib3 v2, re-add support for TLS 1.0 and TLS 1.1.
|
321
|
+
|
322
|
+
https://urllib3.readthedocs.io/en/latest/v2-migration-guide.html#https-requires-tls-1-2
|
323
|
+
"""
|
324
|
+
if Version(urllib3.__version__) >= Version("2"):
|
325
|
+
from urllib3.util import parse_url
|
326
|
+
|
327
|
+
scheme, _, host, port, *_ = parse_url(server)
|
328
|
+
if scheme == "https":
|
329
|
+
kwargs["ssl_minimum_version"] = ssl.TLSVersion.MINIMUM_SUPPORTED
|
330
|
+
|
331
|
+
|
332
|
+
def _create_sql_payload(stmt, args, bulk_args):
|
333
|
+
if not isinstance(stmt, str):
|
334
|
+
raise ValueError("stmt is not a string")
|
335
|
+
if args and bulk_args:
|
336
|
+
raise ValueError("Cannot provide both: args and bulk_args")
|
337
|
+
|
338
|
+
data = {"stmt": stmt}
|
339
|
+
if args:
|
340
|
+
data["args"] = args
|
341
|
+
if bulk_args:
|
342
|
+
data["bulk_args"] = bulk_args
|
343
|
+
return orjson.dumps(
|
344
|
+
data,
|
345
|
+
default=cratedb_json_encoder,
|
346
|
+
option=orjson.OPT_PASSTHROUGH_DATETIME,
|
347
|
+
)
|
348
|
+
|
349
|
+
|
350
|
+
def _get_socket_opts(
|
351
|
+
keepalive=True, tcp_keepidle=None, tcp_keepintvl=None, tcp_keepcnt=None
|
352
|
+
):
|
353
|
+
"""
|
354
|
+
Return an optional list of socket options for urllib3's HTTPConnection
|
355
|
+
constructor.
|
356
|
+
"""
|
357
|
+
if not keepalive:
|
358
|
+
return None
|
359
|
+
|
360
|
+
# always use TCP keepalive
|
361
|
+
opts = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
|
362
|
+
|
363
|
+
# hasattr check because some options depend on system capabilities
|
364
|
+
# see https://docs.python.org/3/library/socket.html#socket.SOMAXCONN
|
365
|
+
if hasattr(socket, "TCP_KEEPIDLE") and tcp_keepidle is not None:
|
366
|
+
opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, tcp_keepidle))
|
367
|
+
if hasattr(socket, "TCP_KEEPINTVL") and tcp_keepintvl is not None:
|
368
|
+
opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, tcp_keepintvl))
|
369
|
+
if hasattr(socket, "TCP_KEEPCNT") and tcp_keepcnt is not None:
|
370
|
+
opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, tcp_keepcnt))
|
371
|
+
|
372
|
+
# additionally use urllib3's default socket options
|
373
|
+
return list(HTTPConnection.default_socket_options) + opts
|
374
|
+
|
375
|
+
|
376
|
+
class Client:
|
377
|
+
"""
|
378
|
+
Crate connection client using CrateDB's HTTP API.
|
379
|
+
"""
|
380
|
+
|
381
|
+
SQL_PATH = "/_sql?types=true"
|
382
|
+
"""Crate URI path for issuing SQL statements."""
|
383
|
+
|
384
|
+
retry_interval = 30
|
385
|
+
"""Retry interval for failed servers in seconds."""
|
386
|
+
|
387
|
+
default_server = "http://127.0.0.1:4200"
|
388
|
+
"""Default server to use if no servers are given on instantiation."""
|
389
|
+
|
390
|
+
def __init__(
|
391
|
+
self,
|
392
|
+
servers=None,
|
393
|
+
timeout=None,
|
394
|
+
backoff_factor=0,
|
395
|
+
verify_ssl_cert=True,
|
396
|
+
ca_cert=None,
|
397
|
+
error_trace=False,
|
398
|
+
cert_file=None,
|
399
|
+
key_file=None,
|
400
|
+
ssl_relax_minimum_version=False,
|
401
|
+
username=None,
|
402
|
+
password=None,
|
403
|
+
schema=None,
|
404
|
+
pool_size=None,
|
405
|
+
socket_keepalive=True,
|
406
|
+
socket_tcp_keepidle=None,
|
407
|
+
socket_tcp_keepintvl=None,
|
408
|
+
socket_tcp_keepcnt=None,
|
409
|
+
):
|
410
|
+
if not servers:
|
411
|
+
servers = [self.default_server]
|
412
|
+
else:
|
413
|
+
servers = _to_server_list(servers)
|
414
|
+
|
415
|
+
# Try to derive credentials from first server argument if not
|
416
|
+
# explicitly given.
|
417
|
+
if servers and not username:
|
418
|
+
try:
|
419
|
+
url = urlparse(servers[0])
|
420
|
+
if url.username is not None:
|
421
|
+
username = url.username
|
422
|
+
if url.password is not None:
|
423
|
+
password = url.password
|
424
|
+
except Exception as ex:
|
425
|
+
logger.warning(
|
426
|
+
"Unable to decode credentials from database "
|
427
|
+
"URI, so connecting to CrateDB without "
|
428
|
+
"authentication: {ex}".format(ex=ex)
|
429
|
+
)
|
430
|
+
|
431
|
+
self._active_servers = servers
|
432
|
+
self._inactive_servers = []
|
433
|
+
pool_kw = _pool_kw_args(
|
434
|
+
verify_ssl_cert,
|
435
|
+
ca_cert,
|
436
|
+
cert_file,
|
437
|
+
key_file,
|
438
|
+
timeout,
|
439
|
+
pool_size,
|
440
|
+
)
|
441
|
+
pool_kw.update(
|
442
|
+
{
|
443
|
+
"socket_keepalive": socket_keepalive,
|
444
|
+
"socket_tcp_keepidle": socket_tcp_keepidle,
|
445
|
+
"socket_tcp_keepintvl": socket_tcp_keepintvl,
|
446
|
+
"socket_tcp_keepcnt": socket_tcp_keepcnt,
|
447
|
+
}
|
448
|
+
)
|
449
|
+
self.ssl_relax_minimum_version = ssl_relax_minimum_version
|
450
|
+
self.backoff_factor = backoff_factor
|
451
|
+
self.server_pool = {}
|
452
|
+
self._update_server_pool(servers, **pool_kw)
|
453
|
+
self._pool_kw = pool_kw
|
454
|
+
self._lock = threading.RLock()
|
455
|
+
self._local = threading.local()
|
456
|
+
self.username = username
|
457
|
+
self.password = password
|
458
|
+
self.schema = schema
|
459
|
+
|
460
|
+
self.path = self.SQL_PATH
|
461
|
+
if error_trace:
|
462
|
+
self.path += "&error_trace=true"
|
463
|
+
|
464
|
+
def close(self):
|
465
|
+
for server in self.server_pool.values():
|
466
|
+
server.close()
|
467
|
+
|
468
|
+
def _create_server(self, server, **pool_kw):
|
469
|
+
kwargs = _remove_certs_for_non_https(server, pool_kw)
|
470
|
+
# After updating to urllib3 v2, optionally retain support
|
471
|
+
# for TLS 1.0 and TLS 1.1, in order to support connectivity
|
472
|
+
# to older versions of CrateDB.
|
473
|
+
if self.ssl_relax_minimum_version:
|
474
|
+
_update_pool_kwargs_for_ssl_minimum_version(server, kwargs)
|
475
|
+
self.server_pool[server] = Server(server, **kwargs)
|
476
|
+
|
477
|
+
def _update_server_pool(self, servers, **pool_kw):
|
478
|
+
for server in servers:
|
479
|
+
self._create_server(server, **pool_kw)
|
480
|
+
|
481
|
+
def sql(self, stmt, parameters=None, bulk_parameters=None):
|
482
|
+
"""
|
483
|
+
Execute SQL stmt against the crate server.
|
484
|
+
"""
|
485
|
+
if stmt is None:
|
486
|
+
return None
|
487
|
+
|
488
|
+
data = _create_sql_payload(stmt, parameters, bulk_parameters)
|
489
|
+
logger.debug("Sending request to %s with payload: %s", self.path, data)
|
490
|
+
content = self._json_request("POST", self.path, data=data)
|
491
|
+
logger.debug("JSON response for stmt(%s): %s", stmt, content)
|
492
|
+
|
493
|
+
return content
|
494
|
+
|
495
|
+
def server_infos(self, server):
|
496
|
+
response = self._request("GET", "/", server=server)
|
497
|
+
_raise_for_status(response)
|
498
|
+
content = _json_from_response(response)
|
499
|
+
node_name = content.get("name")
|
500
|
+
node_version = content.get("version", {}).get("number", "0.0.0")
|
501
|
+
return server, node_name, node_version
|
502
|
+
|
503
|
+
def blob_put(self, table, digest, data) -> bool:
|
504
|
+
"""
|
505
|
+
Stores the contents of the file like @data object in a blob under the
|
506
|
+
given table and digest.
|
507
|
+
"""
|
508
|
+
response = self._request("PUT", _blob_path(table, digest), data=data)
|
509
|
+
if response.status == 201:
|
510
|
+
# blob created
|
511
|
+
return True
|
512
|
+
if response.status == 409:
|
513
|
+
# blob exists
|
514
|
+
return False
|
515
|
+
if response.status in (400, 404):
|
516
|
+
raise BlobLocationNotFoundException(table, digest)
|
517
|
+
_raise_for_status(response)
|
518
|
+
return False
|
519
|
+
|
520
|
+
def blob_del(self, table, digest) -> bool:
|
521
|
+
"""
|
522
|
+
Deletes the blob with given digest under the given table.
|
523
|
+
"""
|
524
|
+
response = self._request("DELETE", _blob_path(table, digest))
|
525
|
+
if response.status == 204:
|
526
|
+
return True
|
527
|
+
if response.status == 404:
|
528
|
+
return False
|
529
|
+
_raise_for_status(response)
|
530
|
+
return False
|
531
|
+
|
532
|
+
def blob_get(self, table, digest, chunk_size=1024 * 128):
|
533
|
+
"""
|
534
|
+
Returns a file like object representing the contents of the blob
|
535
|
+
with the given digest.
|
536
|
+
"""
|
537
|
+
response = self._request("GET", _blob_path(table, digest), stream=True)
|
538
|
+
if response.status == 404:
|
539
|
+
raise DigestNotFoundException(table, digest)
|
540
|
+
_raise_for_status(response)
|
541
|
+
return response.stream(amt=chunk_size)
|
542
|
+
|
543
|
+
def blob_exists(self, table, digest) -> bool:
|
544
|
+
"""
|
545
|
+
Returns true if the blob with the given digest exists
|
546
|
+
under the given table.
|
547
|
+
"""
|
548
|
+
response = self._request("HEAD", _blob_path(table, digest))
|
549
|
+
if response.status == 200:
|
550
|
+
return True
|
551
|
+
elif response.status == 404:
|
552
|
+
return False
|
553
|
+
_raise_for_status(response)
|
554
|
+
return False
|
555
|
+
|
556
|
+
def _add_server(self, server):
|
557
|
+
with self._lock:
|
558
|
+
if server not in self.server_pool:
|
559
|
+
self._create_server(server, **self._pool_kw)
|
560
|
+
|
561
|
+
def _request(self, method, path, server=None, **kwargs):
|
562
|
+
"""Execute a request to the cluster
|
563
|
+
|
564
|
+
A server is selected from the server pool.
|
565
|
+
"""
|
566
|
+
while True:
|
567
|
+
next_server = server or self._get_server()
|
568
|
+
try:
|
569
|
+
response = self.server_pool[next_server].request(
|
570
|
+
method,
|
571
|
+
path,
|
572
|
+
username=self.username,
|
573
|
+
password=self.password,
|
574
|
+
backoff_factor=self.backoff_factor,
|
575
|
+
schema=self.schema,
|
576
|
+
**kwargs,
|
577
|
+
)
|
578
|
+
redirect_location = response.get_redirect_location()
|
579
|
+
if redirect_location and 300 <= response.status <= 308:
|
580
|
+
redirect_server = _server_url(redirect_location)
|
581
|
+
self._add_server(redirect_server)
|
582
|
+
return self._request(
|
583
|
+
method, path, server=redirect_server, **kwargs
|
584
|
+
)
|
585
|
+
if not server and response.status in SRV_UNAVAILABLE_STATUSES:
|
586
|
+
with self._lock:
|
587
|
+
# drop server from active ones
|
588
|
+
self._drop_server(next_server, response.reason)
|
589
|
+
else:
|
590
|
+
return response
|
591
|
+
except (
|
592
|
+
MaxRetryError,
|
593
|
+
ReadTimeoutError,
|
594
|
+
SSLError,
|
595
|
+
HTTPError,
|
596
|
+
ProxyError,
|
597
|
+
) as ex:
|
598
|
+
ex_message = _ex_to_message(ex)
|
599
|
+
if server:
|
600
|
+
raise ConnectionError(
|
601
|
+
"Server not available, exception: %s" % ex_message
|
602
|
+
) from ex
|
603
|
+
preserve_server = False
|
604
|
+
if isinstance(ex, ProtocolError):
|
605
|
+
preserve_server = any(
|
606
|
+
t in [type(arg) for arg in ex.args]
|
607
|
+
for t in PRESERVE_ACTIVE_SERVER_EXCEPTIONS
|
608
|
+
)
|
609
|
+
if not preserve_server:
|
610
|
+
with self._lock:
|
611
|
+
# drop server from active ones
|
612
|
+
self._drop_server(next_server, ex_message)
|
613
|
+
except Exception as e:
|
614
|
+
raise ProgrammingError(_ex_to_message(e)) from e
|
615
|
+
|
616
|
+
def _json_request(self, method, path, data):
|
617
|
+
"""
|
618
|
+
Issue request against the crate HTTP API.
|
619
|
+
"""
|
620
|
+
|
621
|
+
response = self._request(method, path, data=data)
|
622
|
+
_raise_for_status(response)
|
623
|
+
if len(response.data) > 0:
|
624
|
+
return _json_from_response(response)
|
625
|
+
return response.data
|
626
|
+
|
627
|
+
def _get_server(self):
|
628
|
+
"""
|
629
|
+
Get server to use for request.
|
630
|
+
Also process inactive server list, re-add them after given interval.
|
631
|
+
"""
|
632
|
+
with self._lock:
|
633
|
+
inactive_server_count = len(self._inactive_servers)
|
634
|
+
for _ in range(inactive_server_count):
|
635
|
+
try:
|
636
|
+
ts, server, message = heapq.heappop(self._inactive_servers)
|
637
|
+
except IndexError:
|
638
|
+
pass
|
639
|
+
else:
|
640
|
+
if (ts + self.retry_interval) > time():
|
641
|
+
# Not yet, put it back
|
642
|
+
heapq.heappush(
|
643
|
+
self._inactive_servers, (ts, server, message)
|
644
|
+
)
|
645
|
+
else:
|
646
|
+
self._active_servers.append(server)
|
647
|
+
logger.warning(
|
648
|
+
"Restored server %s into active pool", server
|
649
|
+
)
|
650
|
+
|
651
|
+
# if none is old enough, use oldest
|
652
|
+
if not self._active_servers:
|
653
|
+
ts, server, message = heapq.heappop(self._inactive_servers)
|
654
|
+
self._active_servers.append(server)
|
655
|
+
logger.info("Restored server %s into active pool", server)
|
656
|
+
|
657
|
+
server = self._active_servers[0]
|
658
|
+
self._roundrobin()
|
659
|
+
|
660
|
+
return server
|
661
|
+
|
662
|
+
@property
|
663
|
+
def active_servers(self):
|
664
|
+
"""get the active servers for this client"""
|
665
|
+
with self._lock:
|
666
|
+
return list(self._active_servers)
|
667
|
+
|
668
|
+
def _drop_server(self, server, message):
|
669
|
+
"""
|
670
|
+
Drop server from active list and adds it to the inactive ones.
|
671
|
+
"""
|
672
|
+
try:
|
673
|
+
self._active_servers.remove(server)
|
674
|
+
except ValueError:
|
675
|
+
pass
|
676
|
+
else:
|
677
|
+
heapq.heappush(self._inactive_servers, (time(), server, message))
|
678
|
+
logger.warning("Removed server %s from active pool", server)
|
679
|
+
|
680
|
+
# if this is the last server raise exception, otherwise try next
|
681
|
+
if not self._active_servers:
|
682
|
+
raise ConnectionError(
|
683
|
+
("No more Servers available, exception from last server: %s")
|
684
|
+
% message
|
685
|
+
)
|
686
|
+
|
687
|
+
def _roundrobin(self):
|
688
|
+
"""
|
689
|
+
Very simple round-robin implementation
|
690
|
+
"""
|
691
|
+
self._active_servers.append(self._active_servers.pop(0))
|
692
|
+
|
693
|
+
def __repr__(self):
|
694
|
+
return "<Client {0}>".format(str(self._active_servers))
|
File without changes
|