crate 2.2.1b2__tar.gz → 2.3.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.2.1b2 → crate-2.3.0}/CHANGES.rst +27 -0
- {crate-2.2.1b2 → crate-2.3.0}/PKG-INFO +2 -2
- {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/client.rst +4 -8
- {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/cursor.rst +79 -1
- {crate-2.2.1b2 → crate-2.3.0}/docs/connect.rst +20 -1
- {crate-2.2.1b2 → crate-2.3.0}/docs/query.rst +57 -10
- {crate-2.2.1b2 → crate-2.3.0}/pyproject.toml +5 -5
- crate-2.3.0/src/crate/client/__init__.py +49 -0
- crate-2.3.0/src/crate/client/_pep440.py +1 -0
- crate-2.3.0/src/crate/client/blob.py +105 -0
- crate-2.3.0/src/crate/client/connection.py +241 -0
- crate-2.3.0/src/crate/client/converter.py +211 -0
- crate-2.3.0/src/crate/client/cursor.py +431 -0
- crate-2.3.0/src/crate/client/exceptions.py +101 -0
- crate-2.3.0/src/crate/client/http.py +775 -0
- crate-2.3.0/src/crate/testing/layer.py +431 -0
- crate-2.3.0/src/crate/testing/util.py +95 -0
- crate-2.3.0/tests/client/test_connection.py +319 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_cursor.py +375 -12
- {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_http.py +16 -2
- crate-2.3.0/tests/testing/__init__.py +0 -0
- crate-2.2.1b2/examples/README.rst +0 -9
- crate-2.2.1b2/tests/client/test_connection.py +0 -169
- {crate-2.2.1b2 → crate-2.3.0}/.gitignore +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/CONTRIBUTING.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/DEVELOP.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/LICENSE +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/NOTICE +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/README.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/.gitignore +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/Makefile +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/_extra/robots.txt +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/blobs.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/build.json +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/blob.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/connection.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/http.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/https.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/index.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/conf.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/data-types.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/docutils.conf +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/getting-started.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/index-all.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/index.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/other-options.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/docs/requirements.txt +0 -0
- {crate-2.2.1b2/tests → crate-2.3.0/src/crate/testing}/__init__.py +0 -0
- {crate-2.2.1b2/tests/client → crate-2.3.0/tests}/__init__.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/import/test_a.json +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/mappings/locations.sql +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/cacert_invalid.pem +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/cacert_valid.pem +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/client_invalid.pem +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/client_valid.pem +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/readme.rst +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/server_valid.pem +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/assets/settings/test_a.json +0 -0
- {crate-2.2.1b2/tests/testing → crate-2.3.0/tests/client}/__init__.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/client/settings.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_blob.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_exceptions.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_serialization.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_utils.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/conftest.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/test_docs.py +0 -0
- {crate-2.2.1b2 → crate-2.3.0}/tests/testing/test_layer.py +0 -0
|
@@ -2,6 +2,33 @@
|
|
|
2
2
|
Changes for crate
|
|
3
3
|
=================
|
|
4
4
|
|
|
5
|
+
2026/09/17 2.3.0
|
|
6
|
+
================
|
|
7
|
+
- Added ``DefaultTypeConverter`` support that decodes ``DataType.UUID``
|
|
8
|
+
columns to Python ``uuid.UUID`` objects instead of returning the raw string.
|
|
9
|
+
- Added CrateDB column type identifiers to ``DataType``: ``INTERVAL`` (17),
|
|
10
|
+
``ROW`` (18), ``FLOAT_VECTOR`` (28), ``UUID`` (29), and ``REGTYPE`` (30).
|
|
11
|
+
Fixed ``Converter.get()`` raising ``ValueError`` for column type identifiers
|
|
12
|
+
it does not know. Unknown identifiers now fall back to the default converter.
|
|
13
|
+
|
|
14
|
+
- Breaking change: ``connect()`` now raises
|
|
15
|
+
``crate.client.exceptions.ConnectionError`` immediately if no configured
|
|
16
|
+
server node responds.
|
|
17
|
+
|
|
18
|
+
- Breaking change: ``DefaultTypeConverter`` now decodes ``DataType.BIT``
|
|
19
|
+
columns, converting CrateDB's ``B'0110'`` wire format to a plain string of
|
|
20
|
+
``0``/``1`` digits. Code that stripped the wrapper itself needs to be
|
|
21
|
+
adjusted, or can restore the previous behaviour by mapping
|
|
22
|
+
``DataType.BIT`` to a converter of its own.
|
|
23
|
+
|
|
24
|
+
2026/06/17 2.2.1
|
|
25
|
+
================
|
|
26
|
+
|
|
27
|
+
- Fixed ``cursor.execute()`` with ``bulk_parameters`` and pyformat SQL: when
|
|
28
|
+
rows are dicts, both the SQL template and the rows are now converted to
|
|
29
|
+
positional format before sending to CrateDB. Positional-list rows
|
|
30
|
+
continue to work as before.
|
|
31
|
+
|
|
5
32
|
2026/06/04 2.2.0
|
|
6
33
|
==========
|
|
7
34
|
- Added JSON serialization support for Python's ``datetime.time`` type,
|
|
@@ -29,12 +29,8 @@ respond, the request is automatically routed to the next server:
|
|
|
29
29
|
>>> connection = client.connect([invalid_host, crate_host])
|
|
30
30
|
>>> connection.close()
|
|
31
31
|
|
|
32
|
-
If no ``servers`` are
|
|
33
|
-
|
|
34
|
-
>>> connection = client.connect()
|
|
35
|
-
>>> connection.client._active_servers
|
|
36
|
-
['http://127.0.0.1:4200']
|
|
37
|
-
>>> connection.close()
|
|
32
|
+
If no ``servers`` are supplied to the ``connect`` method, the default address
|
|
33
|
+
``http://127.0.0.1:4200`` is used.
|
|
38
34
|
|
|
39
35
|
If the option ``error_trace`` is set to ``True``, the client will print a whole
|
|
40
36
|
traceback if a server error occurs:
|
|
@@ -77,7 +73,7 @@ connect:
|
|
|
77
73
|
|
|
78
74
|
The username for trusted users can also be provided in the URL:
|
|
79
75
|
|
|
80
|
-
>>> connection = client.connect(['
|
|
76
|
+
>>> connection = client.connect([crate_host.replace('://', '://trusted_me@')])
|
|
81
77
|
>>> connection.client.username
|
|
82
78
|
'trusted_me'
|
|
83
79
|
>>> connection.client.password
|
|
@@ -97,7 +93,7 @@ also need to provide ``password`` as argument for the ``connect()`` call:
|
|
|
97
93
|
|
|
98
94
|
The authentication credentials can also be provided in the URL:
|
|
99
95
|
|
|
100
|
-
>>> connection = client.connect(['
|
|
96
|
+
>>> connection = client.connect([crate_host.replace('://', '://me:my_secret_pw@')])
|
|
101
97
|
>>> connection.client.username
|
|
102
98
|
'me'
|
|
103
99
|
>>> connection.client.password
|
|
@@ -266,6 +266,37 @@ For completeness' sake the cursor description is updated nonetheless:
|
|
|
266
266
|
>>> [ desc[0] for desc in cursor.description ]
|
|
267
267
|
['name', 'position']
|
|
268
268
|
|
|
269
|
+
executemany() with named parameters
|
|
270
|
+
====================================
|
|
271
|
+
|
|
272
|
+
``executemany()`` also accepts a :class:`py:list` of :class:`py:dict` when
|
|
273
|
+
the SQL statement contains ``%(name)s`` placeholders. The client converts both the SQL
|
|
274
|
+
template and all rows to positional format before sending them to CrateDB:
|
|
275
|
+
|
|
276
|
+
.. Hidden: set up mocked response
|
|
277
|
+
|
|
278
|
+
>>> connection.client.set_next_response({
|
|
279
|
+
... "results": [
|
|
280
|
+
... {"rowcount": 1},
|
|
281
|
+
... {"rowcount": 1}
|
|
282
|
+
... ],
|
|
283
|
+
... "duration": 123,
|
|
284
|
+
... "cols": [],
|
|
285
|
+
... })
|
|
286
|
+
|
|
287
|
+
>>> cursor = connection.cursor()
|
|
288
|
+
|
|
289
|
+
>>> cursor.executemany(
|
|
290
|
+
... "INSERT INTO t (id, val) VALUES (%(id)s, %(val)s)",
|
|
291
|
+
... [{"id": 1, "val": "foo"}, {"id": 2, "val": "bar"}])
|
|
292
|
+
[{'rowcount': 1}, {'rowcount': 1}]
|
|
293
|
+
|
|
294
|
+
>>> cursor.rowcount
|
|
295
|
+
2
|
|
296
|
+
|
|
297
|
+
>>> cursor.duration
|
|
298
|
+
123
|
|
299
|
+
|
|
269
300
|
>>> connection.client.set_next_response({
|
|
270
301
|
... "rows":[ [ "North West Ripple", 1 ], [ "Arkintoofle Minor", 3 ], [ "Alpha Centauri", 3 ] ],
|
|
271
302
|
... "cols":[ "name", "position" ],
|
|
@@ -312,7 +343,8 @@ Python data type conversion
|
|
|
312
343
|
|
|
313
344
|
The cursor object can optionally convert database types to native Python data
|
|
314
345
|
types. Currently, this is implemented for the CrateDB data types ``IP``,
|
|
315
|
-
``TIMESTAMP``, and ``
|
|
346
|
+
``TIMESTAMP``, ``TIMETZ``, ``BIT``, and ``UUID`` on behalf of the
|
|
347
|
+
``DefaultTypeConverter``.
|
|
316
348
|
|
|
317
349
|
>>> cursor = connection.cursor(converter=DefaultTypeConverter())
|
|
318
350
|
|
|
@@ -348,6 +380,52 @@ and decoded to a ``datetime.time`` object with the appropriate timezone:
|
|
|
348
380
|
[datetime.time(12, 30, 45, tzinfo=datetime.timezone.utc)]
|
|
349
381
|
|
|
350
382
|
|
|
383
|
+
CrateDB's ``BIT`` type is returned over HTTP in its SQL literal form,
|
|
384
|
+
``B'0110'``. It is decoded to a plain string of ``0``/``1`` digits.
|
|
385
|
+
|
|
386
|
+
>>> cursor = connection.cursor(converter=DefaultTypeConverter())
|
|
387
|
+
|
|
388
|
+
.. hide: set up the mocked response::
|
|
389
|
+
|
|
390
|
+
>>> connection.client.set_next_response({
|
|
391
|
+
... "col_types": [25],
|
|
392
|
+
... "rows":[ [ "B'0110'" ] ],
|
|
393
|
+
... "cols":[ "flags" ],
|
|
394
|
+
... "rowcount":1,
|
|
395
|
+
... "duration":1
|
|
396
|
+
... })
|
|
397
|
+
|
|
398
|
+
Executing the query and fetching the decoded result:
|
|
399
|
+
|
|
400
|
+
>>> cursor.execute("select b'0110'")
|
|
401
|
+
|
|
402
|
+
>>> cursor.fetchone()
|
|
403
|
+
['0110']
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
CrateDB's ``UUID`` type is returned over HTTP as a string. It is decoded to a
|
|
407
|
+
Python ``uuid.UUID`` object.
|
|
408
|
+
|
|
409
|
+
>>> cursor = connection.cursor(converter=DefaultTypeConverter())
|
|
410
|
+
|
|
411
|
+
.. hide: set up the mocked response::
|
|
412
|
+
|
|
413
|
+
>>> connection.client.set_next_response({
|
|
414
|
+
... "col_types": [29],
|
|
415
|
+
... "rows":[ [ "a5b3c1e0-1b7f-4f3e-9a2d-6c4e8f0a1b2c" ] ],
|
|
416
|
+
... "cols":[ "id" ],
|
|
417
|
+
... "rowcount":1,
|
|
418
|
+
... "duration":1
|
|
419
|
+
... })
|
|
420
|
+
|
|
421
|
+
Executing the query and fetching the decoded result:
|
|
422
|
+
|
|
423
|
+
>>> cursor.execute("select 'a5b3c1e0-1b7f-4f3e-9a2d-6c4e8f0a1b2c'::uuid")
|
|
424
|
+
|
|
425
|
+
>>> cursor.fetchone()
|
|
426
|
+
[UUID('a5b3c1e0-1b7f-4f3e-9a2d-6c4e8f0a1b2c')]
|
|
427
|
+
|
|
428
|
+
|
|
351
429
|
Custom data type conversion
|
|
352
430
|
===========================
|
|
353
431
|
|
|
@@ -73,13 +73,32 @@ You can pass in as many node URLs as you like.
|
|
|
73
73
|
|
|
74
74
|
.. TIP::
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
When ``connect()`` is called, the client contacts each node to check
|
|
77
|
+
availability and determine the lowest server version. If no node responds,
|
|
78
|
+
a ``ConnectionError`` is raised immediately.
|
|
79
|
+
|
|
80
|
+
For every subsequent query, the client will attempt each node in sequence
|
|
77
81
|
until a successful connection is made. Nodes are moved to the end of the
|
|
78
82
|
list each time they are tried.
|
|
79
83
|
|
|
80
84
|
Over multiple query executions, this behaviour functions as client-side
|
|
81
85
|
*round-robin* load balancing. (This is analogous to `round-robin DNS`_.)
|
|
82
86
|
|
|
87
|
+
.. NOTE::
|
|
88
|
+
|
|
89
|
+
Wrap ``connect()`` in a ``try/except`` block to handle an unreachable
|
|
90
|
+
cluster gracefully:
|
|
91
|
+
|
|
92
|
+
.. code-block:: python
|
|
93
|
+
|
|
94
|
+
from crate import client
|
|
95
|
+
from crate.client.exceptions import ConnectionError
|
|
96
|
+
|
|
97
|
+
try:
|
|
98
|
+
connection = client.connect(["node-1:4200", "node-2:4200"])
|
|
99
|
+
except ConnectionError as e:
|
|
100
|
+
print(f"Could not reach CrateDB cluster: {e}")
|
|
101
|
+
|
|
83
102
|
.. _connection-options:
|
|
84
103
|
|
|
85
104
|
Connection options
|
|
@@ -72,20 +72,19 @@ The same parameter name may appear multiple times in the query:
|
|
|
72
72
|
... "SELECT * FROM locations WHERE name = %(q)s OR kind = %(q)s",
|
|
73
73
|
... {"q": "Quasar"})
|
|
74
74
|
|
|
75
|
-
The client converts the ``%(name)s`` placeholders to positional
|
|
76
|
-
before sending the query to CrateDB
|
|
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.
|
|
75
|
+
The client converts the ``%(name)s`` placeholders to ``$N`` positional
|
|
76
|
+
markers before sending the query to CrateDB.
|
|
83
77
|
|
|
84
78
|
Bulk inserts
|
|
85
79
|
------------
|
|
86
80
|
|
|
87
81
|
:ref:`Bulk inserts <crate-reference:http-bulk-ops>` are possible with the
|
|
88
|
-
``executemany()`` method
|
|
82
|
+
``executemany()`` method.
|
|
83
|
+
|
|
84
|
+
Positional parameters
|
|
85
|
+
.....................
|
|
86
|
+
|
|
87
|
+
Pass a :class:`py:list` of sequences using ``?`` placeholders:
|
|
89
88
|
|
|
90
89
|
>>> cursor.executemany(
|
|
91
90
|
... "INSERT INTO locations (name, date, kind, position) VALUES (?, ?, ?, ?)",
|
|
@@ -94,10 +93,58 @@ Bulk inserts
|
|
|
94
93
|
[{'rowcount': 1}, {'rowcount': 1}]
|
|
95
94
|
|
|
96
95
|
The ``executemany()`` method returns a result :class:`dictionary <py:dict>`
|
|
97
|
-
for every
|
|
96
|
+
for every row. This dictionary always has a ``rowcount`` key, indicating
|
|
98
97
|
how many rows were inserted. If an error occurs, the ``rowcount`` value is
|
|
99
98
|
``-2``, and the dictionary may additionally have an ``error_message`` key.
|
|
100
99
|
|
|
100
|
+
Named parameters
|
|
101
|
+
................
|
|
102
|
+
|
|
103
|
+
``executemany()`` also accepts a :class:`py:list` of :class:`py:dict` using
|
|
104
|
+
``%(name)s`` placeholders. The client converts both the SQL template and all
|
|
105
|
+
rows to positional format before sending to CrateDB:
|
|
106
|
+
|
|
107
|
+
>>> cursor.executemany(
|
|
108
|
+
... "INSERT INTO locations (name, date, kind, position) "
|
|
109
|
+
... "VALUES (%(name)s, %(date)s, %(kind)s, %(pos)s)",
|
|
110
|
+
... [{"name": "Cloverleaf", "date": "2007-03-11", "kind": "Quasar", "pos": 7},
|
|
111
|
+
... {"name": "Old Faithful", "date": "2007-03-11", "kind": "Quasar", "pos": 8}])
|
|
112
|
+
[{'rowcount': 1}, {'rowcount': 1}]
|
|
113
|
+
|
|
114
|
+
Using ``bulk_parameters`` directly
|
|
115
|
+
...................................
|
|
116
|
+
|
|
117
|
+
``execute()`` accepts a ``bulk_parameters`` keyword argument directly:
|
|
118
|
+
|
|
119
|
+
.. NOTE::
|
|
120
|
+
Please prefer ``executemany()`` for bulk inserts, it is the standard DB-API 2.0
|
|
121
|
+
interface. The ``bulk_parameters`` argument is a lower-level alternative.
|
|
122
|
+
|
|
123
|
+
>>> cursor.execute(
|
|
124
|
+
... "INSERT INTO locations (name, kind, position) VALUES (?, ?, ?)",
|
|
125
|
+
... bulk_parameters=[('Cloverleaf', 'Quasar', 7),
|
|
126
|
+
... ('Old Faithful', 'Quasar', 8)])
|
|
127
|
+
|
|
128
|
+
Named ``%(name)s`` placeholders are also supported. When the rows are
|
|
129
|
+
:class:`py:dict` objects the SQL template and rows are fully converted,
|
|
130
|
+
identical to the ``executemany()`` path:
|
|
131
|
+
|
|
132
|
+
>>> cursor.execute(
|
|
133
|
+
... "INSERT INTO locations (name, kind, position) "
|
|
134
|
+
... "VALUES (%(name)s, %(kind)s, %(pos)s)",
|
|
135
|
+
... bulk_parameters=[{"name": "Cloverleaf", "kind": "Quasar", "pos": 7},
|
|
136
|
+
... {"name": "Old Faithful", "kind": "Quasar", "pos": 8}])
|
|
137
|
+
|
|
138
|
+
When the rows are already positional lists (e.g. data coming from a
|
|
139
|
+
DataFrame), only the SQL template is rewritten. In this case the caller must
|
|
140
|
+
ensure the value order in each row matches the placeholder order in the SQL:
|
|
141
|
+
|
|
142
|
+
>>> cursor.execute(
|
|
143
|
+
... "INSERT INTO locations (name, kind, position) "
|
|
144
|
+
... "VALUES (%(name)s, %(kind)s, %(pos)s)",
|
|
145
|
+
... bulk_parameters=[['Cloverleaf', 'Quasar', 7],
|
|
146
|
+
... ['Old Faithful', 'Quasar', 8]])
|
|
147
|
+
|
|
101
148
|
.. _selects:
|
|
102
149
|
|
|
103
150
|
Selecting data
|
|
@@ -5,10 +5,10 @@ build-backend = "hatchling.build"
|
|
|
5
5
|
[tool.hatch.build.targets.sdist]
|
|
6
6
|
include = [
|
|
7
7
|
"/docs",
|
|
8
|
-
"/src/crate
|
|
8
|
+
"/src/crate",
|
|
9
9
|
"/tests",
|
|
10
|
-
"
|
|
11
|
-
"
|
|
10
|
+
"/*.rst",
|
|
11
|
+
"/*.txt",
|
|
12
12
|
]
|
|
13
13
|
exclude = [
|
|
14
14
|
"/docs/.crate-docs",
|
|
@@ -58,10 +58,10 @@ dependencies = [
|
|
|
58
58
|
dev = [
|
|
59
59
|
"certifi",
|
|
60
60
|
"coverage",
|
|
61
|
-
"mypy<2.
|
|
61
|
+
"mypy<2.4",
|
|
62
62
|
"pytest<10",
|
|
63
63
|
"pytz",
|
|
64
|
-
"ruff<0.
|
|
64
|
+
"ruff<0.17",
|
|
65
65
|
]
|
|
66
66
|
docs = [
|
|
67
67
|
"sphinx",
|
|
@@ -0,0 +1,49 @@
|
|
|
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
|
+
from .connection import Connection as connect
|
|
23
|
+
from .exceptions import Error
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"connect",
|
|
27
|
+
"Error",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
# ruff: noqa: E402
|
|
31
|
+
try:
|
|
32
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
33
|
+
except (ImportError, ModuleNotFoundError): # pragma: no cover
|
|
34
|
+
from importlib_metadata import ( # type: ignore[assignment,no-redef,unused-ignore]
|
|
35
|
+
PackageNotFoundError,
|
|
36
|
+
version,
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
__appname__ = "crate"
|
|
40
|
+
|
|
41
|
+
try:
|
|
42
|
+
__version__ = version(__appname__)
|
|
43
|
+
except PackageNotFoundError: # pragma: no cover
|
|
44
|
+
__version__ = "unknown"
|
|
45
|
+
|
|
46
|
+
# codeql[py/unused-global-variable]
|
|
47
|
+
apilevel = "2.0"
|
|
48
|
+
threadsafety = 1
|
|
49
|
+
paramstyle = "pyformat"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from verlib2 import Version # noqa: F401
|
|
@@ -0,0 +1,105 @@
|
|
|
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 hashlib
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class BlobContainer:
|
|
26
|
+
"""class that represents a blob collection in crate.
|
|
27
|
+
|
|
28
|
+
can be used to download, upload and delete blobs
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(self, container_name, connection):
|
|
32
|
+
self.container_name = container_name
|
|
33
|
+
self.conn = connection
|
|
34
|
+
|
|
35
|
+
def _compute_digest(self, f):
|
|
36
|
+
f.seek(0)
|
|
37
|
+
m = hashlib.sha1() # noqa: S324
|
|
38
|
+
while True:
|
|
39
|
+
d = f.read(1024 * 32)
|
|
40
|
+
if not d:
|
|
41
|
+
break
|
|
42
|
+
m.update(d)
|
|
43
|
+
f.seek(0)
|
|
44
|
+
return m.hexdigest()
|
|
45
|
+
|
|
46
|
+
def put(self, f, digest=None):
|
|
47
|
+
"""
|
|
48
|
+
Upload a blob
|
|
49
|
+
|
|
50
|
+
:param f:
|
|
51
|
+
File object to be uploaded (required to support seek if digest is
|
|
52
|
+
not provided).
|
|
53
|
+
:param digest:
|
|
54
|
+
Optional SHA-1 hex digest of the file contents. Gets computed
|
|
55
|
+
before actual upload if not provided, which requires an extra file
|
|
56
|
+
read.
|
|
57
|
+
:return:
|
|
58
|
+
The hex digest of the uploaded blob if not provided in the call.
|
|
59
|
+
Otherwise, a boolean indicating if the blob has been newly created.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
if digest:
|
|
63
|
+
actual_digest = digest
|
|
64
|
+
else:
|
|
65
|
+
actual_digest = self._compute_digest(f)
|
|
66
|
+
|
|
67
|
+
created = self.conn.client.blob_put(
|
|
68
|
+
self.container_name, actual_digest, f
|
|
69
|
+
)
|
|
70
|
+
if digest:
|
|
71
|
+
return created
|
|
72
|
+
return actual_digest
|
|
73
|
+
|
|
74
|
+
def get(self, digest, chunk_size=1024 * 128):
|
|
75
|
+
"""
|
|
76
|
+
Return the contents of a blob
|
|
77
|
+
|
|
78
|
+
:param digest: the hex digest of the blob to return
|
|
79
|
+
:param chunk_size: the size of the chunks returned on each iteration
|
|
80
|
+
:return: generator returning chunks of data
|
|
81
|
+
"""
|
|
82
|
+
return self.conn.client.blob_get(
|
|
83
|
+
self.container_name, digest, chunk_size
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
def delete(self, digest):
|
|
87
|
+
"""
|
|
88
|
+
Delete a blob
|
|
89
|
+
|
|
90
|
+
:param digest: the hex digest of the blob to be deleted
|
|
91
|
+
:return: True if blob existed
|
|
92
|
+
"""
|
|
93
|
+
return self.conn.client.blob_del(self.container_name, digest)
|
|
94
|
+
|
|
95
|
+
def exists(self, digest):
|
|
96
|
+
"""
|
|
97
|
+
Check if a blob exists
|
|
98
|
+
|
|
99
|
+
:param digest: Hex digest of the blob
|
|
100
|
+
:return: Boolean indicating existence of the blob
|
|
101
|
+
"""
|
|
102
|
+
return self.conn.client.blob_exists(self.container_name, digest)
|
|
103
|
+
|
|
104
|
+
def __repr__(self):
|
|
105
|
+
return "<BlobContainer '{0}'>".format(self.container_name)
|