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/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")