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/test_http.py
DELETED
@@ -1,678 +0,0 @@
|
|
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
|
-
import json
|
23
|
-
import time
|
24
|
-
import socket
|
25
|
-
import multiprocessing
|
26
|
-
import sys
|
27
|
-
import os
|
28
|
-
import queue
|
29
|
-
import random
|
30
|
-
import traceback
|
31
|
-
from http.server import BaseHTTPRequestHandler, HTTPServer
|
32
|
-
from multiprocessing.context import ForkProcess
|
33
|
-
from unittest import TestCase
|
34
|
-
from unittest.mock import patch, MagicMock
|
35
|
-
from threading import Thread, Event
|
36
|
-
from decimal import Decimal
|
37
|
-
import datetime as dt
|
38
|
-
|
39
|
-
import urllib3.exceptions
|
40
|
-
from base64 import b64decode
|
41
|
-
from urllib.parse import urlparse, parse_qs
|
42
|
-
|
43
|
-
import uuid
|
44
|
-
import certifi
|
45
|
-
|
46
|
-
from .http import Client, CrateJsonEncoder, _get_socket_opts, _remove_certs_for_non_https
|
47
|
-
from .exceptions import ConnectionError, ProgrammingError, IntegrityError
|
48
|
-
|
49
|
-
REQUEST = 'crate.client.http.Server.request'
|
50
|
-
CA_CERT_PATH = certifi.where()
|
51
|
-
|
52
|
-
|
53
|
-
def fake_request(response=None):
|
54
|
-
def request(*args, **kwargs):
|
55
|
-
if isinstance(response, list):
|
56
|
-
resp = response.pop(0)
|
57
|
-
response.append(resp)
|
58
|
-
return resp
|
59
|
-
elif response:
|
60
|
-
return response
|
61
|
-
else:
|
62
|
-
return MagicMock(spec=urllib3.response.HTTPResponse)
|
63
|
-
return request
|
64
|
-
|
65
|
-
|
66
|
-
def fake_response(status, reason=None, content_type='application/json'):
|
67
|
-
m = MagicMock(spec=urllib3.response.HTTPResponse)
|
68
|
-
m.status = status
|
69
|
-
m.reason = reason or ''
|
70
|
-
m.headers = {'content-type': content_type}
|
71
|
-
return m
|
72
|
-
|
73
|
-
|
74
|
-
def fake_redirect(location):
|
75
|
-
m = fake_response(307)
|
76
|
-
m.get_redirect_location.return_value = location
|
77
|
-
return m
|
78
|
-
|
79
|
-
|
80
|
-
def bad_bulk_response():
|
81
|
-
r = fake_response(400, 'Bad Request')
|
82
|
-
r.data = json.dumps({
|
83
|
-
"results": [
|
84
|
-
{"rowcount": 1},
|
85
|
-
{"error_message": "an error occured"},
|
86
|
-
{"error_message": "another error"},
|
87
|
-
{"error_message": ""},
|
88
|
-
{"error_message": None}
|
89
|
-
]}).encode()
|
90
|
-
return r
|
91
|
-
|
92
|
-
|
93
|
-
def duplicate_key_exception():
|
94
|
-
r = fake_response(409, 'Conflict')
|
95
|
-
r.data = json.dumps({
|
96
|
-
"error": {
|
97
|
-
"code": 4091,
|
98
|
-
"message": "DuplicateKeyException[A document with the same primary key exists already]"
|
99
|
-
}
|
100
|
-
}).encode()
|
101
|
-
return r
|
102
|
-
|
103
|
-
|
104
|
-
def fail_sometimes(*args, **kwargs):
|
105
|
-
if random.randint(1, 100) % 10 == 0:
|
106
|
-
raise urllib3.exceptions.MaxRetryError(None, '/_sql', '')
|
107
|
-
return fake_response(200)
|
108
|
-
|
109
|
-
|
110
|
-
class HttpClientTest(TestCase):
|
111
|
-
|
112
|
-
@patch(REQUEST, fake_request([fake_response(200),
|
113
|
-
fake_response(104, 'Connection reset by peer'),
|
114
|
-
fake_response(503, 'Service Unavailable')]))
|
115
|
-
def test_connection_reset_exception(self):
|
116
|
-
client = Client(servers="localhost:4200")
|
117
|
-
client.sql('select 1')
|
118
|
-
client.sql('select 2')
|
119
|
-
self.assertEqual(['http://localhost:4200'], list(client._active_servers))
|
120
|
-
try:
|
121
|
-
client.sql('select 3')
|
122
|
-
except ProgrammingError:
|
123
|
-
self.assertEqual([], list(client._active_servers))
|
124
|
-
else:
|
125
|
-
self.assertTrue(False)
|
126
|
-
finally:
|
127
|
-
client.close()
|
128
|
-
|
129
|
-
def test_no_connection_exception(self):
|
130
|
-
client = Client()
|
131
|
-
self.assertRaises(ConnectionError, client.sql, 'select foo')
|
132
|
-
client.close()
|
133
|
-
|
134
|
-
@patch(REQUEST)
|
135
|
-
def test_http_error_is_re_raised(self, request):
|
136
|
-
request.side_effect = Exception
|
137
|
-
|
138
|
-
client = Client()
|
139
|
-
self.assertRaises(ProgrammingError, client.sql, 'select foo')
|
140
|
-
client.close()
|
141
|
-
|
142
|
-
@patch(REQUEST)
|
143
|
-
def test_programming_error_contains_http_error_response_content(self, request):
|
144
|
-
request.side_effect = Exception("this shouldn't be raised")
|
145
|
-
|
146
|
-
client = Client()
|
147
|
-
try:
|
148
|
-
client.sql('select 1')
|
149
|
-
except ProgrammingError as e:
|
150
|
-
self.assertEqual("this shouldn't be raised", e.message)
|
151
|
-
else:
|
152
|
-
self.assertTrue(False)
|
153
|
-
finally:
|
154
|
-
client.close()
|
155
|
-
|
156
|
-
@patch(REQUEST, fake_request([fake_response(200),
|
157
|
-
fake_response(503, 'Service Unavailable')]))
|
158
|
-
def test_server_error_50x(self):
|
159
|
-
client = Client(servers="localhost:4200 localhost:4201")
|
160
|
-
client.sql('select 1')
|
161
|
-
client.sql('select 2')
|
162
|
-
try:
|
163
|
-
client.sql('select 3')
|
164
|
-
except ProgrammingError as e:
|
165
|
-
self.assertEqual("No more Servers available, " +
|
166
|
-
"exception from last server: Service Unavailable",
|
167
|
-
e.message)
|
168
|
-
self.assertEqual([], list(client._active_servers))
|
169
|
-
else:
|
170
|
-
self.assertTrue(False)
|
171
|
-
finally:
|
172
|
-
client.close()
|
173
|
-
|
174
|
-
def test_connect(self):
|
175
|
-
client = Client(servers="localhost:4200 localhost:4201")
|
176
|
-
self.assertEqual(client._active_servers,
|
177
|
-
["http://localhost:4200", "http://localhost:4201"])
|
178
|
-
client.close()
|
179
|
-
|
180
|
-
client = Client(servers="localhost:4200")
|
181
|
-
self.assertEqual(client._active_servers, ["http://localhost:4200"])
|
182
|
-
client.close()
|
183
|
-
|
184
|
-
client = Client(servers=["localhost:4200"])
|
185
|
-
self.assertEqual(client._active_servers, ["http://localhost:4200"])
|
186
|
-
client.close()
|
187
|
-
|
188
|
-
client = Client(servers=["localhost:4200", "127.0.0.1:4201"])
|
189
|
-
self.assertEqual(client._active_servers,
|
190
|
-
["http://localhost:4200", "http://127.0.0.1:4201"])
|
191
|
-
client.close()
|
192
|
-
|
193
|
-
@patch(REQUEST, fake_request(fake_redirect('http://localhost:4201')))
|
194
|
-
def test_redirect_handling(self):
|
195
|
-
client = Client(servers='localhost:4200')
|
196
|
-
try:
|
197
|
-
client.blob_get('blobs', 'fake_digest')
|
198
|
-
except ProgrammingError:
|
199
|
-
# 4201 gets added to serverpool but isn't available
|
200
|
-
# that's why we run into an infinite recursion
|
201
|
-
# exception message is: maximum recursion depth exceeded
|
202
|
-
pass
|
203
|
-
self.assertEqual(
|
204
|
-
['http://localhost:4200', 'http://localhost:4201'],
|
205
|
-
sorted(list(client.server_pool.keys()))
|
206
|
-
)
|
207
|
-
# the new non-https server must not contain any SSL only arguments
|
208
|
-
# regression test for github issue #179/#180
|
209
|
-
self.assertEqual(
|
210
|
-
{'socket_options': _get_socket_opts(keepalive=True)},
|
211
|
-
client.server_pool['http://localhost:4201'].pool.conn_kw
|
212
|
-
)
|
213
|
-
client.close()
|
214
|
-
|
215
|
-
@patch(REQUEST)
|
216
|
-
def test_server_infos(self, request):
|
217
|
-
request.side_effect = urllib3.exceptions.MaxRetryError(
|
218
|
-
None, '/', "this shouldn't be raised")
|
219
|
-
client = Client(servers="localhost:4200 localhost:4201")
|
220
|
-
self.assertRaises(
|
221
|
-
ConnectionError, client.server_infos, 'http://localhost:4200')
|
222
|
-
client.close()
|
223
|
-
|
224
|
-
@patch(REQUEST, fake_request(fake_response(503)))
|
225
|
-
def test_server_infos_503(self):
|
226
|
-
client = Client(servers="localhost:4200")
|
227
|
-
self.assertRaises(
|
228
|
-
ConnectionError, client.server_infos, 'http://localhost:4200')
|
229
|
-
client.close()
|
230
|
-
|
231
|
-
@patch(REQUEST, fake_request(
|
232
|
-
fake_response(401, 'Unauthorized', 'text/html')))
|
233
|
-
def test_server_infos_401(self):
|
234
|
-
client = Client(servers="localhost:4200")
|
235
|
-
try:
|
236
|
-
client.server_infos('http://localhost:4200')
|
237
|
-
except ProgrammingError as e:
|
238
|
-
self.assertEqual("401 Client Error: Unauthorized", e.message)
|
239
|
-
else:
|
240
|
-
self.assertTrue(False, msg="Exception should have been raised")
|
241
|
-
finally:
|
242
|
-
client.close()
|
243
|
-
|
244
|
-
@patch(REQUEST, fake_request(bad_bulk_response()))
|
245
|
-
def test_bad_bulk_400(self):
|
246
|
-
client = Client(servers="localhost:4200")
|
247
|
-
try:
|
248
|
-
client.sql("Insert into users (name) values(?)",
|
249
|
-
bulk_parameters=[["douglas"], ["monthy"]])
|
250
|
-
except ProgrammingError as e:
|
251
|
-
self.assertEqual("an error occured\nanother error", e.message)
|
252
|
-
else:
|
253
|
-
self.assertTrue(False, msg="Exception should have been raised")
|
254
|
-
finally:
|
255
|
-
client.close()
|
256
|
-
|
257
|
-
@patch(REQUEST, autospec=True)
|
258
|
-
def test_decimal_serialization(self, request):
|
259
|
-
client = Client(servers="localhost:4200")
|
260
|
-
request.return_value = fake_response(200)
|
261
|
-
|
262
|
-
dec = Decimal(0.12)
|
263
|
-
client.sql('insert into users (float_col) values (?)', (dec,))
|
264
|
-
|
265
|
-
data = json.loads(request.call_args[1]['data'])
|
266
|
-
self.assertEqual(data['args'], [str(dec)])
|
267
|
-
client.close()
|
268
|
-
|
269
|
-
@patch(REQUEST, autospec=True)
|
270
|
-
def test_datetime_is_converted_to_ts(self, request):
|
271
|
-
client = Client(servers="localhost:4200")
|
272
|
-
request.return_value = fake_response(200)
|
273
|
-
|
274
|
-
datetime = dt.datetime(2015, 2, 28, 7, 31, 40)
|
275
|
-
client.sql('insert into users (dt) values (?)', (datetime,))
|
276
|
-
|
277
|
-
# convert string to dict
|
278
|
-
# because the order of the keys isn't deterministic
|
279
|
-
data = json.loads(request.call_args[1]['data'])
|
280
|
-
self.assertEqual(data['args'], [1425108700000])
|
281
|
-
client.close()
|
282
|
-
|
283
|
-
@patch(REQUEST, autospec=True)
|
284
|
-
def test_date_is_converted_to_ts(self, request):
|
285
|
-
client = Client(servers="localhost:4200")
|
286
|
-
request.return_value = fake_response(200)
|
287
|
-
|
288
|
-
day = dt.date(2016, 4, 21)
|
289
|
-
client.sql('insert into users (dt) values (?)', (day,))
|
290
|
-
data = json.loads(request.call_args[1]['data'])
|
291
|
-
self.assertEqual(data['args'], [1461196800000])
|
292
|
-
client.close()
|
293
|
-
|
294
|
-
def test_socket_options_contain_keepalive(self):
|
295
|
-
server = 'http://localhost:4200'
|
296
|
-
client = Client(servers=server)
|
297
|
-
conn_kw = client.server_pool[server].pool.conn_kw
|
298
|
-
self.assertIn(
|
299
|
-
(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), conn_kw['socket_options']
|
300
|
-
)
|
301
|
-
client.close()
|
302
|
-
|
303
|
-
@patch(REQUEST, autospec=True)
|
304
|
-
def test_uuid_serialization(self, request):
|
305
|
-
client = Client(servers="localhost:4200")
|
306
|
-
request.return_value = fake_response(200)
|
307
|
-
|
308
|
-
uid = uuid.uuid4()
|
309
|
-
client.sql('insert into my_table (str_col) values (?)', (uid,))
|
310
|
-
|
311
|
-
data = json.loads(request.call_args[1]['data'])
|
312
|
-
self.assertEqual(data['args'], [str(uid)])
|
313
|
-
client.close()
|
314
|
-
|
315
|
-
@patch(REQUEST, fake_request(duplicate_key_exception()))
|
316
|
-
def test_duplicate_key_error(self):
|
317
|
-
"""
|
318
|
-
Verify that an `IntegrityError` is raised on duplicate key errors,
|
319
|
-
instead of the more general `ProgrammingError`.
|
320
|
-
"""
|
321
|
-
client = Client(servers="localhost:4200")
|
322
|
-
with self.assertRaises(IntegrityError) as cm:
|
323
|
-
client.sql('INSERT INTO testdrive (foo) VALUES (42)')
|
324
|
-
self.assertEqual(cm.exception.message,
|
325
|
-
"DuplicateKeyException[A document with the same primary key exists already]")
|
326
|
-
|
327
|
-
|
328
|
-
@patch(REQUEST, fail_sometimes)
|
329
|
-
class ThreadSafeHttpClientTest(TestCase):
|
330
|
-
"""
|
331
|
-
Using a pool of 5 Threads to emit commands to the multiple servers through
|
332
|
-
one Client-instance
|
333
|
-
|
334
|
-
check if number of servers in _inactive_servers and _active_servers always
|
335
|
-
equals the number of servers initially given.
|
336
|
-
"""
|
337
|
-
servers = [
|
338
|
-
"127.0.0.1:44209",
|
339
|
-
"127.0.0.2:44209",
|
340
|
-
"127.0.0.3:44209",
|
341
|
-
]
|
342
|
-
num_threads = 5
|
343
|
-
num_commands = 1000
|
344
|
-
thread_timeout = 5.0 # seconds
|
345
|
-
|
346
|
-
def __init__(self, *args, **kwargs):
|
347
|
-
self.event = Event()
|
348
|
-
self.err_queue = queue.Queue()
|
349
|
-
super(ThreadSafeHttpClientTest, self).__init__(*args, **kwargs)
|
350
|
-
|
351
|
-
def setUp(self):
|
352
|
-
self.client = Client(self.servers)
|
353
|
-
self.client.retry_interval = 0.2 # faster retry
|
354
|
-
|
355
|
-
def tearDown(self):
|
356
|
-
self.client.close()
|
357
|
-
|
358
|
-
def _run(self):
|
359
|
-
self.event.wait() # wait for the others
|
360
|
-
expected_num_servers = len(self.servers)
|
361
|
-
for x in range(self.num_commands):
|
362
|
-
try:
|
363
|
-
self.client.sql('select name from sys.cluster')
|
364
|
-
except ConnectionError:
|
365
|
-
pass
|
366
|
-
try:
|
367
|
-
with self.client._lock:
|
368
|
-
num_servers = len(self.client._active_servers) + \
|
369
|
-
len(self.client._inactive_servers)
|
370
|
-
self.assertEqual(
|
371
|
-
expected_num_servers,
|
372
|
-
num_servers,
|
373
|
-
"expected %d but got %d" % (expected_num_servers,
|
374
|
-
num_servers)
|
375
|
-
)
|
376
|
-
except AssertionError:
|
377
|
-
self.err_queue.put(sys.exc_info())
|
378
|
-
|
379
|
-
def test_client_threaded(self):
|
380
|
-
"""
|
381
|
-
Testing if lists of servers is handled correctly when client is used
|
382
|
-
from multiple threads with some requests failing.
|
383
|
-
|
384
|
-
**ATTENTION:** this test is probabilistic and does not ensure that the
|
385
|
-
client is indeed thread-safe in all cases, it can only show that it
|
386
|
-
withstands this scenario.
|
387
|
-
"""
|
388
|
-
threads = [
|
389
|
-
Thread(target=self._run, name=str(x))
|
390
|
-
for x in range(self.num_threads)
|
391
|
-
]
|
392
|
-
for thread in threads:
|
393
|
-
thread.start()
|
394
|
-
|
395
|
-
self.event.set()
|
396
|
-
for t in threads:
|
397
|
-
t.join(self.thread_timeout)
|
398
|
-
|
399
|
-
if not self.err_queue.empty():
|
400
|
-
self.assertTrue(False, "".join(
|
401
|
-
traceback.format_exception(*self.err_queue.get(block=False))))
|
402
|
-
|
403
|
-
|
404
|
-
class ClientAddressRequestHandler(BaseHTTPRequestHandler):
|
405
|
-
"""
|
406
|
-
http handler for use with HTTPServer
|
407
|
-
|
408
|
-
returns client host and port in crate-conform-responses
|
409
|
-
"""
|
410
|
-
protocol_version = 'HTTP/1.1'
|
411
|
-
|
412
|
-
def do_GET(self):
|
413
|
-
content_length = self.headers.get("content-length")
|
414
|
-
if content_length:
|
415
|
-
self.rfile.read(int(content_length))
|
416
|
-
response = json.dumps({
|
417
|
-
"cols": ["host", "port"],
|
418
|
-
"rows": [
|
419
|
-
self.client_address[0],
|
420
|
-
self.client_address[1]
|
421
|
-
],
|
422
|
-
"rowCount": 1,
|
423
|
-
})
|
424
|
-
self.send_response(200)
|
425
|
-
self.send_header("Content-Length", len(response))
|
426
|
-
self.send_header("Content-Type", "application/json; charset=UTF-8")
|
427
|
-
self.end_headers()
|
428
|
-
self.wfile.write(response.encode('UTF-8'))
|
429
|
-
|
430
|
-
do_POST = do_PUT = do_DELETE = do_HEAD = do_GET
|
431
|
-
|
432
|
-
|
433
|
-
class KeepAliveClientTest(TestCase):
|
434
|
-
|
435
|
-
server_address = ("127.0.0.1", 65535)
|
436
|
-
|
437
|
-
def __init__(self, *args, **kwargs):
|
438
|
-
super(KeepAliveClientTest, self).__init__(*args, **kwargs)
|
439
|
-
self.server_process = ForkProcess(target=self._run_server)
|
440
|
-
|
441
|
-
def setUp(self):
|
442
|
-
super(KeepAliveClientTest, self).setUp()
|
443
|
-
self.client = Client(["%s:%d" % self.server_address])
|
444
|
-
self.server_process.start()
|
445
|
-
time.sleep(.10)
|
446
|
-
|
447
|
-
def tearDown(self):
|
448
|
-
self.server_process.terminate()
|
449
|
-
self.client.close()
|
450
|
-
super(KeepAliveClientTest, self).tearDown()
|
451
|
-
|
452
|
-
def _run_server(self):
|
453
|
-
self.server = HTTPServer(self.server_address,
|
454
|
-
ClientAddressRequestHandler)
|
455
|
-
self.server.handle_request()
|
456
|
-
|
457
|
-
def test_client_keepalive(self):
|
458
|
-
for x in range(10):
|
459
|
-
result = self.client.sql("select * from fake")
|
460
|
-
|
461
|
-
another_result = self.client.sql("select again from fake")
|
462
|
-
self.assertEqual(result, another_result)
|
463
|
-
|
464
|
-
|
465
|
-
class ParamsTest(TestCase):
|
466
|
-
|
467
|
-
def test_params(self):
|
468
|
-
client = Client(['127.0.0.1:4200'], error_trace=True)
|
469
|
-
parsed = urlparse(client.path)
|
470
|
-
params = parse_qs(parsed.query)
|
471
|
-
self.assertEqual(params["error_trace"], ["true"])
|
472
|
-
client.close()
|
473
|
-
|
474
|
-
def test_no_params(self):
|
475
|
-
client = Client()
|
476
|
-
self.assertEqual(client.path, "/_sql?types=true")
|
477
|
-
client.close()
|
478
|
-
|
479
|
-
|
480
|
-
class RequestsCaBundleTest(TestCase):
|
481
|
-
|
482
|
-
def test_open_client(self):
|
483
|
-
os.environ["REQUESTS_CA_BUNDLE"] = CA_CERT_PATH
|
484
|
-
try:
|
485
|
-
Client('http://127.0.0.1:4200')
|
486
|
-
except ProgrammingError:
|
487
|
-
self.fail("HTTP not working with REQUESTS_CA_BUNDLE")
|
488
|
-
finally:
|
489
|
-
os.unsetenv('REQUESTS_CA_BUNDLE')
|
490
|
-
os.environ["REQUESTS_CA_BUNDLE"] = ''
|
491
|
-
|
492
|
-
def test_remove_certs_for_non_https(self):
|
493
|
-
d = _remove_certs_for_non_https('https', {"ca_certs": 1})
|
494
|
-
self.assertIn('ca_certs', d)
|
495
|
-
|
496
|
-
kwargs = {'ca_certs': 1, 'foobar': 2, 'cert_file': 3}
|
497
|
-
d = _remove_certs_for_non_https('http', kwargs)
|
498
|
-
self.assertNotIn('ca_certs', d)
|
499
|
-
self.assertNotIn('cert_file', d)
|
500
|
-
self.assertIn('foobar', d)
|
501
|
-
|
502
|
-
|
503
|
-
class TimeoutRequestHandler(BaseHTTPRequestHandler):
|
504
|
-
"""
|
505
|
-
HTTP handler for use with TestingHTTPServer
|
506
|
-
updates the shared counter and waits so that the client times out
|
507
|
-
"""
|
508
|
-
|
509
|
-
def do_POST(self):
|
510
|
-
self.server.SHARED['count'] += 1
|
511
|
-
time.sleep(5)
|
512
|
-
|
513
|
-
|
514
|
-
class SharedStateRequestHandler(BaseHTTPRequestHandler):
|
515
|
-
"""
|
516
|
-
HTTP handler for use with TestingHTTPServer
|
517
|
-
sets the shared state of the server and returns an empty response
|
518
|
-
"""
|
519
|
-
|
520
|
-
def do_POST(self):
|
521
|
-
self.server.SHARED['count'] += 1
|
522
|
-
self.server.SHARED['schema'] = self.headers.get('Default-Schema')
|
523
|
-
|
524
|
-
if self.headers.get('Authorization') is not None:
|
525
|
-
auth_header = self.headers['Authorization'].replace('Basic ', '')
|
526
|
-
credentials = b64decode(auth_header).decode('utf-8').split(":", 1)
|
527
|
-
self.server.SHARED['username'] = credentials[0]
|
528
|
-
if len(credentials) > 1 and credentials[1]:
|
529
|
-
self.server.SHARED['password'] = credentials[1]
|
530
|
-
else:
|
531
|
-
self.server.SHARED['password'] = None
|
532
|
-
else:
|
533
|
-
self.server.SHARED['username'] = None
|
534
|
-
|
535
|
-
if self.headers.get('X-User') is not None:
|
536
|
-
self.server.SHARED['usernameFromXUser'] = self.headers['X-User']
|
537
|
-
else:
|
538
|
-
self.server.SHARED['usernameFromXUser'] = None
|
539
|
-
|
540
|
-
# send empty response
|
541
|
-
response = '{}'
|
542
|
-
self.send_response(200)
|
543
|
-
self.send_header("Content-Length", len(response))
|
544
|
-
self.send_header("Content-Type", "application/json; charset=UTF-8")
|
545
|
-
self.end_headers()
|
546
|
-
self.wfile.write(response.encode('utf-8'))
|
547
|
-
|
548
|
-
|
549
|
-
class TestingHTTPServer(HTTPServer):
|
550
|
-
"""
|
551
|
-
http server providing a shared dict
|
552
|
-
"""
|
553
|
-
manager = multiprocessing.Manager()
|
554
|
-
SHARED = manager.dict()
|
555
|
-
SHARED['count'] = 0
|
556
|
-
SHARED['usernameFromXUser'] = None
|
557
|
-
SHARED['username'] = None
|
558
|
-
SHARED['password'] = None
|
559
|
-
SHARED['schema'] = None
|
560
|
-
|
561
|
-
@classmethod
|
562
|
-
def run_server(cls, server_address, request_handler_cls):
|
563
|
-
cls(server_address, request_handler_cls).serve_forever()
|
564
|
-
|
565
|
-
|
566
|
-
class TestingHttpServerTestCase(TestCase):
|
567
|
-
|
568
|
-
def __init__(self, *args, **kwargs):
|
569
|
-
super().__init__(*args, **kwargs)
|
570
|
-
self.assertIsNotNone(self.request_handler)
|
571
|
-
self.server_address = ('127.0.0.1', random.randint(65000, 65535))
|
572
|
-
self.server_process = ForkProcess(target=TestingHTTPServer.run_server,
|
573
|
-
args=(self.server_address, self.request_handler))
|
574
|
-
|
575
|
-
def setUp(self):
|
576
|
-
self.server_process.start()
|
577
|
-
self.wait_for_server()
|
578
|
-
|
579
|
-
def wait_for_server(self):
|
580
|
-
while True:
|
581
|
-
try:
|
582
|
-
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
583
|
-
s.connect(self.server_address)
|
584
|
-
except Exception:
|
585
|
-
time.sleep(.25)
|
586
|
-
else:
|
587
|
-
break
|
588
|
-
|
589
|
-
def tearDown(self):
|
590
|
-
self.server_process.terminate()
|
591
|
-
|
592
|
-
def clientWithKwargs(self, **kwargs):
|
593
|
-
return Client(["%s:%d" % self.server_address], timeout=1, **kwargs)
|
594
|
-
|
595
|
-
|
596
|
-
class RetryOnTimeoutServerTest(TestingHttpServerTestCase):
|
597
|
-
|
598
|
-
request_handler = TimeoutRequestHandler
|
599
|
-
|
600
|
-
def setUp(self):
|
601
|
-
super().setUp()
|
602
|
-
self.client = self.clientWithKwargs()
|
603
|
-
|
604
|
-
def tearDown(self):
|
605
|
-
super().tearDown()
|
606
|
-
self.client.close()
|
607
|
-
|
608
|
-
def test_no_retry_on_read_timeout(self):
|
609
|
-
try:
|
610
|
-
self.client.sql("select * from fake")
|
611
|
-
except ConnectionError as e:
|
612
|
-
self.assertIn('Read timed out', e.message,
|
613
|
-
msg='Error message must contain: Read timed out')
|
614
|
-
self.assertEqual(TestingHTTPServer.SHARED['count'], 1)
|
615
|
-
|
616
|
-
|
617
|
-
class TestDefaultSchemaHeader(TestingHttpServerTestCase):
|
618
|
-
|
619
|
-
request_handler = SharedStateRequestHandler
|
620
|
-
|
621
|
-
def setUp(self):
|
622
|
-
super().setUp()
|
623
|
-
self.client = self.clientWithKwargs(schema='my_custom_schema')
|
624
|
-
|
625
|
-
def tearDown(self):
|
626
|
-
self.client.close()
|
627
|
-
super().tearDown()
|
628
|
-
|
629
|
-
def test_default_schema(self):
|
630
|
-
self.client.sql('SELECT 1')
|
631
|
-
self.assertEqual(TestingHTTPServer.SHARED['schema'], 'my_custom_schema')
|
632
|
-
|
633
|
-
|
634
|
-
class TestUsernameSentAsHeader(TestingHttpServerTestCase):
|
635
|
-
|
636
|
-
request_handler = SharedStateRequestHandler
|
637
|
-
|
638
|
-
def setUp(self):
|
639
|
-
super().setUp()
|
640
|
-
self.clientWithoutUsername = self.clientWithKwargs()
|
641
|
-
self.clientWithUsername = self.clientWithKwargs(username='testDBUser')
|
642
|
-
self.clientWithUsernameAndPassword = self.clientWithKwargs(username='testDBUser',
|
643
|
-
password='test:password')
|
644
|
-
|
645
|
-
def tearDown(self):
|
646
|
-
self.clientWithoutUsername.close()
|
647
|
-
self.clientWithUsername.close()
|
648
|
-
self.clientWithUsernameAndPassword.close()
|
649
|
-
super().tearDown()
|
650
|
-
|
651
|
-
def test_username(self):
|
652
|
-
self.clientWithoutUsername.sql("select * from fake")
|
653
|
-
self.assertEqual(TestingHTTPServer.SHARED['usernameFromXUser'], None)
|
654
|
-
self.assertEqual(TestingHTTPServer.SHARED['username'], None)
|
655
|
-
self.assertEqual(TestingHTTPServer.SHARED['password'], None)
|
656
|
-
|
657
|
-
self.clientWithUsername.sql("select * from fake")
|
658
|
-
self.assertEqual(TestingHTTPServer.SHARED['usernameFromXUser'], 'testDBUser')
|
659
|
-
self.assertEqual(TestingHTTPServer.SHARED['username'], 'testDBUser')
|
660
|
-
self.assertEqual(TestingHTTPServer.SHARED['password'], None)
|
661
|
-
|
662
|
-
self.clientWithUsernameAndPassword.sql("select * from fake")
|
663
|
-
self.assertEqual(TestingHTTPServer.SHARED['usernameFromXUser'], 'testDBUser')
|
664
|
-
self.assertEqual(TestingHTTPServer.SHARED['username'], 'testDBUser')
|
665
|
-
self.assertEqual(TestingHTTPServer.SHARED['password'], 'test:password')
|
666
|
-
|
667
|
-
|
668
|
-
class TestCrateJsonEncoder(TestCase):
|
669
|
-
|
670
|
-
def test_naive_datetime(self):
|
671
|
-
data = dt.datetime.fromisoformat("2023-06-26T09:24:00.123")
|
672
|
-
result = json.dumps(data, cls=CrateJsonEncoder)
|
673
|
-
self.assertEqual(result, "1687771440123")
|
674
|
-
|
675
|
-
def test_aware_datetime(self):
|
676
|
-
data = dt.datetime.fromisoformat("2023-06-26T09:24:00.123+02:00")
|
677
|
-
result = json.dumps(data, cls=CrateJsonEncoder)
|
678
|
-
self.assertEqual(result, "1687764240123")
|