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.
Files changed (67) hide show
  1. {crate-2.2.1b2 → crate-2.3.0}/CHANGES.rst +27 -0
  2. {crate-2.2.1b2 → crate-2.3.0}/PKG-INFO +2 -2
  3. {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/client.rst +4 -8
  4. {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/cursor.rst +79 -1
  5. {crate-2.2.1b2 → crate-2.3.0}/docs/connect.rst +20 -1
  6. {crate-2.2.1b2 → crate-2.3.0}/docs/query.rst +57 -10
  7. {crate-2.2.1b2 → crate-2.3.0}/pyproject.toml +5 -5
  8. crate-2.3.0/src/crate/client/__init__.py +49 -0
  9. crate-2.3.0/src/crate/client/_pep440.py +1 -0
  10. crate-2.3.0/src/crate/client/blob.py +105 -0
  11. crate-2.3.0/src/crate/client/connection.py +241 -0
  12. crate-2.3.0/src/crate/client/converter.py +211 -0
  13. crate-2.3.0/src/crate/client/cursor.py +431 -0
  14. crate-2.3.0/src/crate/client/exceptions.py +101 -0
  15. crate-2.3.0/src/crate/client/http.py +775 -0
  16. crate-2.3.0/src/crate/testing/layer.py +431 -0
  17. crate-2.3.0/src/crate/testing/util.py +95 -0
  18. crate-2.3.0/tests/client/test_connection.py +319 -0
  19. {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_cursor.py +375 -12
  20. {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_http.py +16 -2
  21. crate-2.3.0/tests/testing/__init__.py +0 -0
  22. crate-2.2.1b2/examples/README.rst +0 -9
  23. crate-2.2.1b2/tests/client/test_connection.py +0 -169
  24. {crate-2.2.1b2 → crate-2.3.0}/.gitignore +0 -0
  25. {crate-2.2.1b2 → crate-2.3.0}/CONTRIBUTING.rst +0 -0
  26. {crate-2.2.1b2 → crate-2.3.0}/DEVELOP.rst +0 -0
  27. {crate-2.2.1b2 → crate-2.3.0}/LICENSE +0 -0
  28. {crate-2.2.1b2 → crate-2.3.0}/NOTICE +0 -0
  29. {crate-2.2.1b2 → crate-2.3.0}/README.rst +0 -0
  30. {crate-2.2.1b2 → crate-2.3.0}/docs/.gitignore +0 -0
  31. {crate-2.2.1b2 → crate-2.3.0}/docs/Makefile +0 -0
  32. {crate-2.2.1b2 → crate-2.3.0}/docs/_extra/robots.txt +0 -0
  33. {crate-2.2.1b2 → crate-2.3.0}/docs/blobs.rst +0 -0
  34. {crate-2.2.1b2 → crate-2.3.0}/docs/build.json +0 -0
  35. {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/blob.rst +0 -0
  36. {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/connection.rst +0 -0
  37. {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/http.rst +0 -0
  38. {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/https.rst +0 -0
  39. {crate-2.2.1b2 → crate-2.3.0}/docs/by-example/index.rst +0 -0
  40. {crate-2.2.1b2 → crate-2.3.0}/docs/conf.py +0 -0
  41. {crate-2.2.1b2 → crate-2.3.0}/docs/data-types.rst +0 -0
  42. {crate-2.2.1b2 → crate-2.3.0}/docs/docutils.conf +0 -0
  43. {crate-2.2.1b2 → crate-2.3.0}/docs/getting-started.rst +0 -0
  44. {crate-2.2.1b2 → crate-2.3.0}/docs/index-all.rst +0 -0
  45. {crate-2.2.1b2 → crate-2.3.0}/docs/index.rst +0 -0
  46. {crate-2.2.1b2 → crate-2.3.0}/docs/other-options.rst +0 -0
  47. {crate-2.2.1b2 → crate-2.3.0}/docs/requirements.txt +0 -0
  48. {crate-2.2.1b2/tests → crate-2.3.0/src/crate/testing}/__init__.py +0 -0
  49. {crate-2.2.1b2/tests/client → crate-2.3.0/tests}/__init__.py +0 -0
  50. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/import/test_a.json +0 -0
  51. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/mappings/locations.sql +0 -0
  52. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/cacert_invalid.pem +0 -0
  53. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/cacert_valid.pem +0 -0
  54. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/client_invalid.pem +0 -0
  55. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/client_valid.pem +0 -0
  56. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/readme.rst +0 -0
  57. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/pki/server_valid.pem +0 -0
  58. {crate-2.2.1b2 → crate-2.3.0}/tests/assets/settings/test_a.json +0 -0
  59. {crate-2.2.1b2/tests/testing → crate-2.3.0/tests/client}/__init__.py +0 -0
  60. {crate-2.2.1b2 → crate-2.3.0}/tests/client/settings.py +0 -0
  61. {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_blob.py +0 -0
  62. {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_exceptions.py +0 -0
  63. {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_serialization.py +0 -0
  64. {crate-2.2.1b2 → crate-2.3.0}/tests/client/test_utils.py +0 -0
  65. {crate-2.2.1b2 → crate-2.3.0}/tests/conftest.py +0 -0
  66. {crate-2.2.1b2 → crate-2.3.0}/tests/test_docs.py +0 -0
  67. {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,
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: crate
3
- Version: 2.2.1b2
3
+ Version: 2.3.0
4
4
  Summary: CrateDB Python Client
5
5
  Author-email: "Crate.io" <office@crate.io>
6
6
  License-Expression: Apache-2.0
@@ -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 given, the default one ``http://127.0.0.1:4200`` is used:
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(['http://trusted_me@' + crate_host])
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(['http://me:my_secret_pw@' + crate_host])
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 ``TIMETZ`` on behalf of the ``DefaultTypeConverter``.
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
- For every query, the client will attempt to connect to each node in sequence
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 ``?`` 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.
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, which takes a :class:`py:list` of tuples to insert:
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 tuple. This dictionary always has a ``rowcount`` key, indicating
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/*.py",
8
+ "/src/crate",
9
9
  "/tests",
10
- "*.rst",
11
- "*.txt",
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.2",
61
+ "mypy<2.4",
62
62
  "pytest<10",
63
63
  "pytz",
64
- "ruff<0.16",
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)