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