crate 2.1.2__tar.gz → 2.2.0__tar.gz
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-2.1.2 → crate-2.2.0}/CHANGES.rst +16 -1
- {crate-2.1.2 → crate-2.2.0}/PKG-INFO +1 -1
- {crate-2.1.2 → crate-2.2.0}/docs/by-example/cursor.rst +20 -2
- {crate-2.1.2 → crate-2.2.0}/docs/connect.rst +26 -0
- {crate-2.1.2 → crate-2.2.0}/docs/query.rst +27 -0
- {crate-2.1.2 → crate-2.2.0}/pyproject.toml +1 -1
- {crate-2.1.2 → crate-2.2.0}/tests/client/test_cursor.py +73 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/test_http.py +123 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/test_serialization.py +38 -0
- {crate-2.1.2 → crate-2.2.0}/tests/testing/test_layer.py +5 -15
- {crate-2.1.2 → crate-2.2.0}/.gitignore +0 -0
- {crate-2.1.2 → crate-2.2.0}/CONTRIBUTING.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/DEVELOP.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/LICENSE +0 -0
- {crate-2.1.2 → crate-2.2.0}/NOTICE +0 -0
- {crate-2.1.2 → crate-2.2.0}/README.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/.gitignore +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/Makefile +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/_extra/robots.txt +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/blobs.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/build.json +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/by-example/blob.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/by-example/client.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/by-example/connection.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/by-example/http.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/by-example/https.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/by-example/index.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/conf.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/data-types.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/docutils.conf +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/getting-started.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/index-all.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/index.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/other-options.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/docs/requirements.txt +0 -0
- {crate-2.1.2 → crate-2.2.0}/examples/README.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/__init__.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/import/test_a.json +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/mappings/locations.sql +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/pki/cacert_invalid.pem +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/pki/cacert_valid.pem +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/pki/client_invalid.pem +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/pki/client_valid.pem +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/pki/readme.rst +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/pki/server_valid.pem +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/assets/settings/test_a.json +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/__init__.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/settings.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/test_blob.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/test_connection.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/test_exceptions.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/client/test_utils.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/conftest.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/test_docs.py +0 -0
- {crate-2.1.2 → crate-2.2.0}/tests/testing/__init__.py +0 -0
|
@@ -2,8 +2,23 @@
|
|
|
2
2
|
Changes for crate
|
|
3
3
|
=================
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
2026/06/04 2.2.0
|
|
6
6
|
==========
|
|
7
|
+
- Added JSON serialization support for Python's ``datetime.time`` type,
|
|
8
|
+
encoding it as an ISO 8601 string compatible with CrateDB's ``TIMETZ``
|
|
9
|
+
column type.
|
|
10
|
+
|
|
11
|
+
- Added gzip compression for outgoing request bodies via the ``compress``
|
|
12
|
+
parameter (default: ``8192`` bytes).
|
|
13
|
+
Pass ``True`` to always compress, ``False`` to disable, or an integer
|
|
14
|
+
as a byte threshold. The driver always sends ``Accept-Encoding: gzip,
|
|
15
|
+
deflate`` to negotiate compressed responses from the server when
|
|
16
|
+
compression is enabled.
|
|
17
|
+
|
|
18
|
+
- Added named parameter support (``pyformat`` paramstyle). Passing a
|
|
19
|
+
:class:`py:dict` as ``parameters`` to ``cursor.execute()`` now accepts
|
|
20
|
+
``%(name)s`` placeholders and converts them to positional ``?`` markers
|
|
21
|
+
client-side. Positional parameters using ``?`` continue to work unchanged.
|
|
7
22
|
|
|
8
23
|
2026/03/09 2.1.2
|
|
9
24
|
================
|
|
@@ -311,8 +311,8 @@ Python data type conversion
|
|
|
311
311
|
===========================
|
|
312
312
|
|
|
313
313
|
The cursor object can optionally convert database types to native Python data
|
|
314
|
-
types. Currently, this is implemented for the CrateDB data types ``IP
|
|
315
|
-
``TIMESTAMP`` on behalf of the ``DefaultTypeConverter``.
|
|
314
|
+
types. Currently, this is implemented for the CrateDB data types ``IP``,
|
|
315
|
+
``TIMESTAMP``, and ``TIMETZ`` on behalf of the ``DefaultTypeConverter``.
|
|
316
316
|
|
|
317
317
|
>>> cursor = connection.cursor(converter=DefaultTypeConverter())
|
|
318
318
|
|
|
@@ -329,6 +329,24 @@ types. Currently, this is implemented for the CrateDB data types ``IP`` and
|
|
|
329
329
|
>>> cursor.fetchone()
|
|
330
330
|
['foo', IPv4Address('10.10.10.1'), datetime.datetime(2022, 7, 18, 18, 10, 36, 758000, tzinfo=datetime.timezone.utc)]
|
|
331
331
|
|
|
332
|
+
CrateDB's ``TIMETZ`` type is returned over HTTP as ``[microseconds_since_midnight, tz_offset_seconds]``
|
|
333
|
+
and decoded to a ``datetime.time`` object with the appropriate timezone:
|
|
334
|
+
|
|
335
|
+
>>> cursor = connection.cursor(converter=DefaultTypeConverter())
|
|
336
|
+
|
|
337
|
+
>>> connection.client.set_next_response({
|
|
338
|
+
... "col_types": [20],
|
|
339
|
+
... "rows":[ [ [45045000000, 0] ] ],
|
|
340
|
+
... "cols":[ "t" ],
|
|
341
|
+
... "rowcount":1,
|
|
342
|
+
... "duration":1
|
|
343
|
+
... })
|
|
344
|
+
|
|
345
|
+
>>> cursor.execute('')
|
|
346
|
+
|
|
347
|
+
>>> cursor.fetchone()
|
|
348
|
+
[datetime.time(12, 30, 45, tzinfo=datetime.timezone.utc)]
|
|
349
|
+
|
|
332
350
|
|
|
333
351
|
Custom data type conversion
|
|
334
352
|
===========================
|
|
@@ -266,6 +266,32 @@ with the rest of your arguments.
|
|
|
266
266
|
|
|
267
267
|
However, you can query any schema you like by specifying it in the query.
|
|
268
268
|
|
|
269
|
+
.. _compression:
|
|
270
|
+
|
|
271
|
+
Request and response compression
|
|
272
|
+
=================================
|
|
273
|
+
|
|
274
|
+
The ``compress`` parameter controls gzip compression of outgoing request
|
|
275
|
+
bodies. The default ``8192`` compresses payloads larger than 8 KB::
|
|
276
|
+
|
|
277
|
+
>>> connection = client.connect('localhost:4200')
|
|
278
|
+
# compress=8192 is the default — payloads > 8 KB are gzip-compressed
|
|
279
|
+
|
|
280
|
+
To always compress, regardless of payload size::
|
|
281
|
+
|
|
282
|
+
>>> connection = client.connect('localhost:4200', compress=True)
|
|
283
|
+
|
|
284
|
+
To disable compression entirely::
|
|
285
|
+
|
|
286
|
+
>>> connection = client.connect('localhost:4200', compress=False)
|
|
287
|
+
|
|
288
|
+
To use a custom threshold (bytes)::
|
|
289
|
+
|
|
290
|
+
>>> connection = client.connect('localhost:4200', compress=4096)
|
|
291
|
+
|
|
292
|
+
The driver always sends ``Accept-Encoding: gzip, deflate`` so the server
|
|
293
|
+
may return compressed responses if compression is enabled.
|
|
294
|
+
|
|
269
295
|
Next steps
|
|
270
296
|
==========
|
|
271
297
|
|
|
@@ -54,6 +54,33 @@ characters appear, in the order they appear.
|
|
|
54
54
|
Always use the parameter interpolation feature of the client library to
|
|
55
55
|
guard against malicious input, as demonstrated in the example above.
|
|
56
56
|
|
|
57
|
+
Named parameters
|
|
58
|
+
----------------
|
|
59
|
+
|
|
60
|
+
For queries with many parameters or repeated values, named parameters improve
|
|
61
|
+
readability. Pass a :class:`py:dict` as the second argument using
|
|
62
|
+
``%(name)s`` placeholders:
|
|
63
|
+
|
|
64
|
+
>>> cursor.execute(
|
|
65
|
+
... "INSERT INTO locations (name, date, kind, position) "
|
|
66
|
+
... "VALUES (%(name)s, %(date)s, %(kind)s, %(pos)s)",
|
|
67
|
+
... {"name": "Einstein Cross", "date": "2007-03-11", "kind": "Quasar", "pos": 7})
|
|
68
|
+
|
|
69
|
+
The same parameter name may appear multiple times in the query:
|
|
70
|
+
|
|
71
|
+
>>> cursor.execute(
|
|
72
|
+
... "SELECT * FROM locations WHERE name = %(q)s OR kind = %(q)s",
|
|
73
|
+
... {"q": "Quasar"})
|
|
74
|
+
|
|
75
|
+
The client converts the ``%(name)s`` placeholders to positional ``?`` markers
|
|
76
|
+
before sending the query to CrateDB, so no server-side changes are required.
|
|
77
|
+
|
|
78
|
+
.. NOTE::
|
|
79
|
+
|
|
80
|
+
Named parameters are not yet supported by ``executemany()``. Use
|
|
81
|
+
positional ``?`` placeholders with a :class:`py:list` of tuples for bulk
|
|
82
|
+
operations.
|
|
83
|
+
|
|
57
84
|
Bulk inserts
|
|
58
85
|
------------
|
|
59
86
|
|
|
@@ -293,6 +293,41 @@ def test_execute_custom_converter(mocked_connection):
|
|
|
293
293
|
]
|
|
294
294
|
|
|
295
295
|
|
|
296
|
+
def test_execute_time_converter(mocked_connection):
|
|
297
|
+
"""
|
|
298
|
+
Verify that CrateDB's TIMETZ wire format
|
|
299
|
+
``[microseconds, tz_offset_seconds]`` is decoded to a ``datetime.time``
|
|
300
|
+
object by ``DefaultTypeConverter``.
|
|
301
|
+
"""
|
|
302
|
+
converter = DefaultTypeConverter()
|
|
303
|
+
cursor = mocked_connection.cursor(converter=converter)
|
|
304
|
+
response = {
|
|
305
|
+
"col_types": [20],
|
|
306
|
+
"cols": ["t"],
|
|
307
|
+
"rows": [
|
|
308
|
+
[[45045000000, 0]], # 12:30:45 UTC
|
|
309
|
+
[[45045123456, 7200]], # 12:30:45.123456 +02:00
|
|
310
|
+
[None],
|
|
311
|
+
],
|
|
312
|
+
"rowcount": 3,
|
|
313
|
+
"duration": 1,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
with mock.patch.object(
|
|
317
|
+
mocked_connection.client, "sql", return_value=response
|
|
318
|
+
):
|
|
319
|
+
cursor.execute("")
|
|
320
|
+
result = cursor.fetchall()
|
|
321
|
+
|
|
322
|
+
assert result == [
|
|
323
|
+
[datetime.time(12, 30, 45, 0,
|
|
324
|
+
tzinfo=datetime.timezone.utc)],
|
|
325
|
+
[datetime.time(12, 30, 45, 123456,
|
|
326
|
+
tzinfo=datetime.timezone(datetime.timedelta(hours=2)))],
|
|
327
|
+
[None],
|
|
328
|
+
]
|
|
329
|
+
|
|
330
|
+
|
|
296
331
|
def test_execute_with_converter_and_invalid_data_type(mocked_connection):
|
|
297
332
|
converter = DefaultTypeConverter()
|
|
298
333
|
|
|
@@ -492,6 +527,44 @@ def test_execute_with_timezone(mocked_connection):
|
|
|
492
527
|
assert result[0][1].tzname() == "UTC"
|
|
493
528
|
|
|
494
529
|
|
|
530
|
+
def test_execute_with_named_params(mocked_connection):
|
|
531
|
+
"""
|
|
532
|
+
Verify that named %(name)s parameters are converted to positional ? markers
|
|
533
|
+
and the values are passed as an ordered list.
|
|
534
|
+
"""
|
|
535
|
+
cursor = mocked_connection.cursor()
|
|
536
|
+
cursor.execute(
|
|
537
|
+
"SELECT * FROM t WHERE a = %(a)s AND b = %(b)s",
|
|
538
|
+
{"a": 1, "b": 2},
|
|
539
|
+
)
|
|
540
|
+
mocked_connection.client.sql.assert_called_once_with(
|
|
541
|
+
"SELECT * FROM t WHERE a = $1 AND b = $2", [1, 2], None
|
|
542
|
+
)
|
|
543
|
+
|
|
544
|
+
|
|
545
|
+
def test_execute_with_named_params_repeated(mocked_connection):
|
|
546
|
+
"""
|
|
547
|
+
Verify that a parameter name used multiple times in the SQL is resolved
|
|
548
|
+
correctly each time it appears.
|
|
549
|
+
"""
|
|
550
|
+
cursor = mocked_connection.cursor()
|
|
551
|
+
cursor.execute("SELECT %(x)s, %(x)s", {"x": 42})
|
|
552
|
+
mocked_connection.client.sql.assert_called_once_with(
|
|
553
|
+
"SELECT $1, $1", [42], None
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
|
|
557
|
+
def test_execute_with_named_params_missing(mocked_connection):
|
|
558
|
+
"""
|
|
559
|
+
Verify that a ProgrammingError is raised when a placeholder name is absent
|
|
560
|
+
from the parameters dict, and that the client is never called.
|
|
561
|
+
"""
|
|
562
|
+
cursor = mocked_connection.cursor()
|
|
563
|
+
with pytest.raises(ProgrammingError, match="Named parameter 'z' not found"):
|
|
564
|
+
cursor.execute("SELECT %(z)s", {"a": 1})
|
|
565
|
+
mocked_connection.client.sql.assert_not_called()
|
|
566
|
+
|
|
567
|
+
|
|
495
568
|
def test_cursor_close(mocked_connection):
|
|
496
569
|
"""
|
|
497
570
|
Verify that a cursor is not closed if not specifically closed.
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
# with Crate these terms will supersede the license and you may use the
|
|
20
20
|
# software solely pursuant to the terms of the relevant commercial agreement.
|
|
21
21
|
|
|
22
|
+
import gzip
|
|
22
23
|
import json
|
|
23
24
|
import os
|
|
24
25
|
import queue
|
|
@@ -200,6 +201,48 @@ def test_redirect_handling():
|
|
|
200
201
|
assert conn_kw == {"socket_options": _get_socket_opts(keepalive=True)}
|
|
201
202
|
|
|
202
203
|
|
|
204
|
+
@pytest.mark.parametrize("method,args,success_status", [
|
|
205
|
+
("blob_exists", ("blobs", "fake_digest"), 200),
|
|
206
|
+
("blob_put", ("blobs", "fake_digest", b"data"), 201),
|
|
207
|
+
("blob_del", ("blobs", "fake_digest"), 204),
|
|
208
|
+
("blob_get", ("blobs", "fake_digest"), 200),
|
|
209
|
+
])
|
|
210
|
+
def test_redirect_blob_preserves_basic_auth(method, args, success_status):
|
|
211
|
+
"""
|
|
212
|
+
Verify Basic HTTP auth credentials are forwarded when following blob
|
|
213
|
+
endpoint redirects.
|
|
214
|
+
"""
|
|
215
|
+
redirect = fake_redirect("http://localhost:4201/_blobs/blobs/fake_digest")
|
|
216
|
+
success = fake_response(success_status)
|
|
217
|
+
|
|
218
|
+
with patch(REQUEST_PATH, side_effect=[redirect, success]) as mock_req:
|
|
219
|
+
client = Client(
|
|
220
|
+
servers="localhost:4200", username="admin", password="secret"
|
|
221
|
+
)
|
|
222
|
+
getattr(client, method)(*args)
|
|
223
|
+
|
|
224
|
+
assert mock_req.call_count == 2
|
|
225
|
+
for call in mock_req.call_args_list:
|
|
226
|
+
assert call.kwargs.get("username") == "admin"
|
|
227
|
+
assert call.kwargs.get("password") == "secret"
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def test_redirect_blob_preserves_jwt_auth():
|
|
231
|
+
"""
|
|
232
|
+
Verify JWT bearer token is forwarded when following blob endpoint redirects.
|
|
233
|
+
"""
|
|
234
|
+
redirect = fake_redirect("http://localhost:4201/_blobs/blobs/fake_digest")
|
|
235
|
+
success = fake_response(200)
|
|
236
|
+
|
|
237
|
+
with patch(REQUEST_PATH, side_effect=[redirect, success]) as mock_req:
|
|
238
|
+
client = Client(servers="localhost:4200", jwt_token="my.jwt.token")
|
|
239
|
+
client.blob_exists("blobs", "fake_digest")
|
|
240
|
+
|
|
241
|
+
assert mock_req.call_count == 2
|
|
242
|
+
for call in mock_req.call_args_list:
|
|
243
|
+
assert call.kwargs.get("jwt_token") == "my.jwt.token"
|
|
244
|
+
|
|
245
|
+
|
|
203
246
|
def test_server_infos():
|
|
204
247
|
"""
|
|
205
248
|
Verify that when a `MaxRetryError` is raised, a `ConnectionError` is raised.
|
|
@@ -735,3 +778,83 @@ def test_credentials_and_token(serve_http):
|
|
|
735
778
|
assert excinfo.match(
|
|
736
779
|
"Either JWT tokens are accepted, or user credentials, but not both"
|
|
737
780
|
)
|
|
781
|
+
|
|
782
|
+
def test_compress_accept_encoding_always_sent():
|
|
783
|
+
"""Accept-Encoding is sent even when compression is disabled."""
|
|
784
|
+
captured = {}
|
|
785
|
+
|
|
786
|
+
def capturing(*_, **kwargs):
|
|
787
|
+
captured["headers"] = kwargs.get("headers") or {}
|
|
788
|
+
return fake_response(200)
|
|
789
|
+
|
|
790
|
+
with patch(REQUEST_PATH, side_effect=capturing):
|
|
791
|
+
Client(servers="localhost:4200", compress=False).sql("SELECT 1")
|
|
792
|
+
assert captured["headers"].get("Accept-Encoding") == "gzip, deflate"
|
|
793
|
+
|
|
794
|
+
|
|
795
|
+
def test_compress_false_no_content_encoding():
|
|
796
|
+
"""No Content-Encoding header when compress=False."""
|
|
797
|
+
captured = {}
|
|
798
|
+
|
|
799
|
+
def capturing(*_, **kwargs):
|
|
800
|
+
captured["headers"] = kwargs.get("headers") or {}
|
|
801
|
+
return fake_response(200)
|
|
802
|
+
|
|
803
|
+
with patch(REQUEST_PATH, side_effect=capturing):
|
|
804
|
+
Client(servers="localhost:4200", compress=False).sql("SELECT 1")
|
|
805
|
+
assert "Content-Encoding" not in captured["headers"]
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
def test_compress_true_always_compresses():
|
|
809
|
+
"""compress=True compresses regardless of payload size."""
|
|
810
|
+
captured = {}
|
|
811
|
+
|
|
812
|
+
def capturing(*_, **kwargs):
|
|
813
|
+
captured["data"] = kwargs.get("data", b"")
|
|
814
|
+
captured["headers"] = kwargs.get("headers") or {}
|
|
815
|
+
return fake_response(200)
|
|
816
|
+
|
|
817
|
+
with patch(REQUEST_PATH, side_effect=capturing):
|
|
818
|
+
Client(servers="localhost:4200", compress=True).sql("SELECT 1")
|
|
819
|
+
assert captured["headers"].get("Content-Encoding") == "gzip"
|
|
820
|
+
assert b'"stmt"' in gzip.decompress(captured["data"])
|
|
821
|
+
|
|
822
|
+
|
|
823
|
+
def test_compress_threshold_above():
|
|
824
|
+
"""Payload above threshold is compressed."""
|
|
825
|
+
captured = {}
|
|
826
|
+
|
|
827
|
+
def capturing(*_, **kwargs):
|
|
828
|
+
captured["headers"] = kwargs.get("headers") or {}
|
|
829
|
+
return fake_response(200)
|
|
830
|
+
|
|
831
|
+
with patch(REQUEST_PATH, side_effect=capturing):
|
|
832
|
+
Client(servers="localhost:4200", compress=0).sql("SELECT 1")
|
|
833
|
+
assert captured["headers"].get("Content-Encoding") == "gzip"
|
|
834
|
+
|
|
835
|
+
|
|
836
|
+
def test_compress_threshold_below():
|
|
837
|
+
"""Payload below threshold is not compressed."""
|
|
838
|
+
captured = {}
|
|
839
|
+
|
|
840
|
+
def capturing(*_, **kwargs):
|
|
841
|
+
captured["headers"] = kwargs.get("headers") or {}
|
|
842
|
+
return fake_response(200)
|
|
843
|
+
|
|
844
|
+
with patch(REQUEST_PATH, side_effect=capturing):
|
|
845
|
+
Client(servers="localhost:4200", compress=999_999).sql("SELECT 1")
|
|
846
|
+
assert "Content-Encoding" not in captured["headers"]
|
|
847
|
+
|
|
848
|
+
|
|
849
|
+
def test_compress_default():
|
|
850
|
+
"""Default args: Accept-Encoding sent, small payload not compressed."""
|
|
851
|
+
captured = {}
|
|
852
|
+
|
|
853
|
+
def capturing(*_, **kwargs):
|
|
854
|
+
captured["headers"] = kwargs.get("headers") or {}
|
|
855
|
+
return fake_response(200)
|
|
856
|
+
|
|
857
|
+
with patch(REQUEST_PATH, side_effect=capturing):
|
|
858
|
+
Client(servers="localhost:4200").sql("SELECT 1")
|
|
859
|
+
assert captured["headers"].get("Accept-Encoding") == "gzip, deflate"
|
|
860
|
+
assert "Content-Encoding" not in captured["headers"]
|
|
@@ -125,6 +125,44 @@ def test_date_serialization():
|
|
|
125
125
|
assert result == b"1461196800000"
|
|
126
126
|
|
|
127
127
|
|
|
128
|
+
def test_naive_time_serialization():
|
|
129
|
+
"""
|
|
130
|
+
Verify that a naive `datetime.time` serializes to an ISO 8601 string.
|
|
131
|
+
"""
|
|
132
|
+
data = dt.time(12, 30, 45)
|
|
133
|
+
result = json_dumps(data)
|
|
134
|
+
assert result == b'"12:30:45"'
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def test_time_with_microseconds_serialization():
|
|
138
|
+
"""
|
|
139
|
+
Verify that `datetime.time` with microseconds serializes correctly.
|
|
140
|
+
"""
|
|
141
|
+
data = dt.time(12, 30, 45, 123456)
|
|
142
|
+
result = json_dumps(data)
|
|
143
|
+
assert result == b'"12:30:45.123456"'
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def test_aware_time_serialization():
|
|
147
|
+
"""
|
|
148
|
+
Verify that a timezone-aware `datetime.time` serializes to ISO 8601 format,
|
|
149
|
+
including the UTC offset.
|
|
150
|
+
"""
|
|
151
|
+
data = dt.time(12, 30, 45, tzinfo=dt.timezone.utc)
|
|
152
|
+
result = json_dumps(data)
|
|
153
|
+
assert result == b'"12:30:45+00:00"'
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def test_aware_time_with_offset_serialization():
|
|
157
|
+
"""
|
|
158
|
+
Verify that a `datetime.time` with a non-UTC offset serializes correctly.
|
|
159
|
+
"""
|
|
160
|
+
tz = dt.timezone(dt.timedelta(hours=2))
|
|
161
|
+
data = dt.time(12, 30, 45, tzinfo=tz)
|
|
162
|
+
result = json_dumps(data)
|
|
163
|
+
assert result == b'"12:30:45+02:00"'
|
|
164
|
+
|
|
165
|
+
|
|
128
166
|
def test_uuid_serialization():
|
|
129
167
|
"""
|
|
130
168
|
Verify that a `uuid.UUID` can be serialized.
|
|
@@ -22,13 +22,11 @@
|
|
|
22
22
|
import json
|
|
23
23
|
import os
|
|
24
24
|
import tempfile
|
|
25
|
-
import urllib
|
|
26
25
|
from io import BytesIO
|
|
27
26
|
from pathlib import Path
|
|
28
27
|
from unittest import TestCase, mock
|
|
29
28
|
|
|
30
29
|
import urllib3
|
|
31
|
-
from verlib2 import Version
|
|
32
30
|
|
|
33
31
|
import crate
|
|
34
32
|
from crate.testing.layer import (
|
|
@@ -38,7 +36,7 @@ from crate.testing.layer import (
|
|
|
38
36
|
wait_for_http_url,
|
|
39
37
|
)
|
|
40
38
|
from tests.client.settings import crate_path
|
|
41
|
-
from tests.conftest import download_cratedb
|
|
39
|
+
from tests.conftest import download_cratedb, get_crate_url
|
|
42
40
|
|
|
43
41
|
|
|
44
42
|
class LayerUtilsTest(TestCase):
|
|
@@ -86,18 +84,10 @@ class LayerUtilsTest(TestCase):
|
|
|
86
84
|
The CrateLayer can also be created by providing an URI that points to
|
|
87
85
|
a CrateDB tarball.
|
|
88
86
|
"""
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
version = versions["crate_testing"]
|
|
94
|
-
|
|
95
|
-
self.assertGreaterEqual(Version(version), Version("4.5.0"))
|
|
96
|
-
|
|
97
|
-
uri = "https://cdn.crate.io/downloads/releases/crate-{}.tar.gz".format(
|
|
98
|
-
version
|
|
99
|
-
)
|
|
100
|
-
layer = CrateLayer.from_uri(uri, name="crate-by-uri", http_port=42203)
|
|
87
|
+
layer = CrateLayer.from_uri(get_crate_url(),
|
|
88
|
+
name="crate-by-uri",
|
|
89
|
+
http_port=42203
|
|
90
|
+
)
|
|
101
91
|
self.assertIsInstance(layer, CrateLayer)
|
|
102
92
|
|
|
103
93
|
@mock.patch.dict("os.environ", {}, clear=True)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|