zerobucket 0.26.0__tar.gz → 0.26.1__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.
- {zerobucket-0.26.0 → zerobucket-0.26.1}/PKG-INFO +1 -1
- {zerobucket-0.26.0 → zerobucket-0.26.1}/pyproject.toml +1 -1
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/__init__.py +1 -1
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/sqlite.py +86 -8
- zerobucket-0.26.1/tests/test_sqlite_stream_fallback.py +160 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/.gitignore +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/README.md +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/_ids.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/__init__.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/base.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/base_async.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/mysql.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/mysql_async.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/postgres.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/postgres_async.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/adapters/sqlite_async.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/async_client.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/cli.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/client.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/content_types.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/exceptions.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/object_storage.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/optimization.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/py.typed +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/types.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/validation.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/validators/__init__.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/src/zerobucket/validators/pdf.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/__init__.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/conftest.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/photo_fixtures.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_access_control.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_async_client.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_batch.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_cli.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_client.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_custom_validators.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_dedup.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_errors.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_malformed_ids.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_mysql_adapter.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_mysql_async_adapter.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_optimization.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_pdf_validator.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_pool_and_metrics.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_retry.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_sqlite_adapter.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_sqlite_async_adapter.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_streaming.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_tiering.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_transactions.py +0 -0
- {zerobucket-0.26.0 → zerobucket-0.26.1}/tests/test_validation.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: zerobucket
|
|
3
|
-
Version: 0.26.
|
|
3
|
+
Version: 0.26.1
|
|
4
4
|
Summary: Database-native image storage. Your database. Your images. Zero buckets.
|
|
5
5
|
Project-URL: Homepage, https://github.com/KedarGhadyalji/ZeroBucket
|
|
6
6
|
Project-URL: Repository, https://github.com/KedarGhadyalji/ZeroBucket
|
|
@@ -48,11 +48,14 @@ than left for someone to wonder about while reading unfamiliar code:
|
|
|
48
48
|
a dynamically-sized `IN (?, ?, ...)` placeholder list instead.
|
|
49
49
|
- `RETURNING` IS supported (SQLite >= 3.35, released March 2021) and is
|
|
50
50
|
used the same way the Postgres adapter uses it.
|
|
51
|
-
- get_stream() uses `sqlite3.Connection.blobopen()`
|
|
52
|
-
genuine incremental-BLOB-I/O API -- not a
|
|
53
|
-
substring()-style range queries the way the
|
|
54
|
-
version is. This
|
|
55
|
-
|
|
51
|
+
- get_stream() uses `sqlite3.Connection.blobopen()` where it exists
|
|
52
|
+
(Python >= 3.11), a genuine incremental-BLOB-I/O API -- not a
|
|
53
|
+
workaround built on top of substring()-style range queries the way the
|
|
54
|
+
Postgres adapter's version is. This package supports Python 3.10 too,
|
|
55
|
+
which has no blobopen(); there get_stream() reads the same bytes with
|
|
56
|
+
substr() range queries inside ONE held read transaction, which gives
|
|
57
|
+
the same snapshot behaviour (see `_stream_with_substr`). Both paths are
|
|
58
|
+
covered by tests/test_sqlite_stream_fallback.py on every Python.
|
|
56
59
|
"""
|
|
57
60
|
|
|
58
61
|
from __future__ import annotations
|
|
@@ -67,6 +70,13 @@ from ..exceptions import StorageError
|
|
|
67
70
|
from ..object_storage import ObjectStorage
|
|
68
71
|
from .base import StorageBackend, StoredRecord, StoredRecordMetadata
|
|
69
72
|
|
|
73
|
+
# Connection.blobopen() (incremental BLOB I/O) was added in Python 3.11; this package
|
|
74
|
+
# supports 3.10. Where it is missing, get_stream() reads the same bytes with substr()
|
|
75
|
+
# range queries inside one held read transaction. A module-level flag, not an inline
|
|
76
|
+
# hasattr(), so the fallback can be exercised on newer Pythons too (the tests switch
|
|
77
|
+
# it off).
|
|
78
|
+
_HAS_BLOBOPEN = hasattr(sqlite3.Connection, "blobopen")
|
|
79
|
+
|
|
70
80
|
DEFAULT_STREAM_CHUNK_SIZE = 1024 * 1024
|
|
71
81
|
|
|
72
82
|
# 10 seconds. See _connect()'s docstring for why this is set explicitly
|
|
@@ -250,6 +260,60 @@ def _now_iso() -> str:
|
|
|
250
260
|
return datetime.now(timezone.utc).isoformat()
|
|
251
261
|
|
|
252
262
|
|
|
263
|
+
def _stream_with_substr(
|
|
264
|
+
conn: sqlite3.Connection,
|
|
265
|
+
table: str,
|
|
266
|
+
rowid: int,
|
|
267
|
+
total_size: int,
|
|
268
|
+
chunk_size: int,
|
|
269
|
+
image_id: str,
|
|
270
|
+
owns_connection: bool,
|
|
271
|
+
) -> Iterator[bytes]:
|
|
272
|
+
"""get_stream() for Pythons without Connection.blobopen() (3.10).
|
|
273
|
+
|
|
274
|
+
Reads the bytes with `substr()` range queries, one per chunk, but inside ONE read
|
|
275
|
+
transaction held open for the whole stream. In WAL mode that transaction pins a
|
|
276
|
+
snapshot, so a stream that has started keeps returning the original bytes even if
|
|
277
|
+
another connection deletes the row meanwhile: the same behaviour as the blobopen()
|
|
278
|
+
path (and unlike the Postgres and async SQLite adapters, whose per-chunk queries
|
|
279
|
+
each see the current state). If the caller passed their own connection that is
|
|
280
|
+
already inside a transaction, that transaction is used as it is and left open.
|
|
281
|
+
|
|
282
|
+
`table` is one of two constants chosen by the caller, never user input.
|
|
283
|
+
"""
|
|
284
|
+
began = False
|
|
285
|
+
try:
|
|
286
|
+
if not conn.in_transaction:
|
|
287
|
+
try:
|
|
288
|
+
conn.execute("BEGIN")
|
|
289
|
+
began = True
|
|
290
|
+
except Exception as exc: # noqa: BLE001
|
|
291
|
+
raise StorageError(f"Failed to stream image: {exc}") from exc
|
|
292
|
+
delivered = 0
|
|
293
|
+
while delivered < total_size:
|
|
294
|
+
want = min(chunk_size, total_size - delivered)
|
|
295
|
+
try:
|
|
296
|
+
row = conn.execute(
|
|
297
|
+
f"SELECT substr(data, ?, ?) FROM {table} WHERE rowid = ?;",
|
|
298
|
+
(delivered + 1, want, rowid), # substr() is 1-indexed
|
|
299
|
+
).fetchone()
|
|
300
|
+
except Exception as exc: # noqa: BLE001
|
|
301
|
+
raise StorageError(f"Failed to stream image: {exc}") from exc
|
|
302
|
+
chunk = bytes(row[0]) if row is not None and row[0] is not None else b""
|
|
303
|
+
if not chunk:
|
|
304
|
+
raise StorageError(
|
|
305
|
+
f"Image {image_id!r} was deleted while streaming "
|
|
306
|
+
f"(delivered {delivered} of {total_size} bytes)."
|
|
307
|
+
)
|
|
308
|
+
yield chunk
|
|
309
|
+
delivered += len(chunk)
|
|
310
|
+
finally:
|
|
311
|
+
if began:
|
|
312
|
+
conn.rollback() # ends the read transaction; nothing was written
|
|
313
|
+
if owns_connection:
|
|
314
|
+
conn.close()
|
|
315
|
+
|
|
316
|
+
|
|
253
317
|
class SQLiteBackend(StorageBackend):
|
|
254
318
|
"""Storage backend for SQLite. See module docstring for what's
|
|
255
319
|
implemented in this phase and what isn't yet.
|
|
@@ -642,8 +706,10 @@ class SQLiteBackend(StorageBackend):
|
|
|
642
706
|
chunk_size: int = DEFAULT_STREAM_CHUNK_SIZE,
|
|
643
707
|
connection: sqlite3.Connection | None = None,
|
|
644
708
|
) -> Iterator[bytes] | None:
|
|
645
|
-
"""Uses `sqlite3.Connection.blobopen()` -- a real
|
|
646
|
-
BLOB-read API, not a range-query workaround
|
|
709
|
+
"""Uses `sqlite3.Connection.blobopen()` on Python 3.11+ -- a real
|
|
710
|
+
incremental-BLOB-read API, not a range-query workaround -- and
|
|
711
|
+
`substr()` range queries in one held read transaction on 3.10,
|
|
712
|
+
which has no blobopen() (see `_stream_with_substr`). Unlike the Postgres
|
|
647
713
|
adapter (which does one `_run()` call PER CHUNK, each its own
|
|
648
714
|
pooled connection), this holds ONE connection open for the
|
|
649
715
|
WHOLE stream -- necessary because the Blob handle is tied to
|
|
@@ -663,7 +729,8 @@ class SQLiteBackend(StorageBackend):
|
|
|
663
729
|
deleted by another connection mid-stream causes the NEXT chunk
|
|
664
730
|
fetch to see no row and raise StorageError. On SQLite, this
|
|
665
731
|
method holds ONE connection open for the whole stream, and
|
|
666
|
-
|
|
732
|
+
the read transaction (blobopen()'s own on Python 3.11+, an explicit
|
|
733
|
+
one on 3.10, where blobopen() does not exist) gives it a
|
|
667
734
|
consistent snapshot (in WAL mode) of the row as it was when
|
|
668
735
|
streaming began -- a concurrent DELETE from another connection
|
|
669
736
|
does NOT interrupt or truncate an in-progress stream here; the
|
|
@@ -732,6 +799,17 @@ class SQLiteBackend(StorageBackend):
|
|
|
732
799
|
table = "zerobucket_images"
|
|
733
800
|
|
|
734
801
|
def generator() -> Iterator[bytes]:
|
|
802
|
+
if not _HAS_BLOBOPEN:
|
|
803
|
+
yield from _stream_with_substr(
|
|
804
|
+
conn,
|
|
805
|
+
table,
|
|
806
|
+
rowid,
|
|
807
|
+
total_size,
|
|
808
|
+
chunk_size,
|
|
809
|
+
image_id,
|
|
810
|
+
owns_connection,
|
|
811
|
+
)
|
|
812
|
+
return
|
|
735
813
|
try:
|
|
736
814
|
blob = conn.blobopen(table, "data", rowid, readonly=True)
|
|
737
815
|
except Exception as exc: # noqa: BLE001
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"""SQLiteBackend.get_stream() on Pythons without Connection.blobopen() (3.10).
|
|
2
|
+
|
|
3
|
+
blobopen() (incremental BLOB I/O) arrived in Python 3.11, but this package supports 3.10,
|
|
4
|
+
so get_stream() has a fallback that reads the same bytes with substr() range queries
|
|
5
|
+
inside one held read transaction. These tests run every scenario through BOTH
|
|
6
|
+
implementations by switching the module flag, so the fallback is exercised on 3.11 and
|
|
7
|
+
3.12 as well, not only on the one Python where it is the only option. (On 3.10 the
|
|
8
|
+
"blobopen" variant is skipped: there is nothing to switch to.)
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import io
|
|
14
|
+
import os
|
|
15
|
+
import sqlite3
|
|
16
|
+
import tempfile
|
|
17
|
+
|
|
18
|
+
import pytest
|
|
19
|
+
from PIL import Image as PILImage
|
|
20
|
+
|
|
21
|
+
from zerobucket import ZeroBucket
|
|
22
|
+
from zerobucket.adapters import sqlite as sqlite_mod
|
|
23
|
+
from zerobucket.adapters.sqlite import SQLiteBackend
|
|
24
|
+
from zerobucket.exceptions import StorageError
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@pytest.fixture(params=["blobopen", "substr"])
|
|
28
|
+
def impl(request, monkeypatch):
|
|
29
|
+
if request.param == "blobopen" and not hasattr(sqlite3.Connection, "blobopen"):
|
|
30
|
+
pytest.skip("this Python has no Connection.blobopen (3.10): only the fallback exists")
|
|
31
|
+
monkeypatch.setattr(sqlite_mod, "_HAS_BLOBOPEN", request.param == "blobopen")
|
|
32
|
+
return request.param
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@pytest.fixture
|
|
36
|
+
def path():
|
|
37
|
+
p = tempfile.mktemp(suffix=".db")
|
|
38
|
+
yield p
|
|
39
|
+
for ext in ("", "-wal", "-shm"):
|
|
40
|
+
if os.path.exists(p + ext):
|
|
41
|
+
os.remove(p + ext)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@pytest.fixture(params=["classic", "dedup"])
|
|
45
|
+
def zb(request, path):
|
|
46
|
+
z = ZeroBucket(backend=SQLiteBackend(path, dedup=(request.param == "dedup")))
|
|
47
|
+
yield z
|
|
48
|
+
z.close()
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _noise_jpeg(side: int = 300) -> bytes:
|
|
52
|
+
"""A JPEG of random pixels: large for its dimensions and different on every call."""
|
|
53
|
+
img = PILImage.frombytes("RGB", (side, side), os.urandom(side * side * 3))
|
|
54
|
+
buf = io.BytesIO()
|
|
55
|
+
img.save(buf, format="JPEG", quality=95)
|
|
56
|
+
return buf.getvalue()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _delete_the_bytes_from_another_connection(path: str, image_id: str, dedup: bool) -> None:
|
|
60
|
+
"""Delete the row get_stream() actually reads from: the image row, or in dedup mode the
|
|
61
|
+
shared blob row (deleting only the reference would leave the bytes in place and prove
|
|
62
|
+
nothing)."""
|
|
63
|
+
other = sqlite3.connect(path)
|
|
64
|
+
try:
|
|
65
|
+
if dedup:
|
|
66
|
+
# these tests store exactly one distinct image
|
|
67
|
+
other.execute("DELETE FROM zerobucket_blobs")
|
|
68
|
+
else:
|
|
69
|
+
other.execute("DELETE FROM zerobucket_images WHERE id = ?", (image_id,))
|
|
70
|
+
other.commit()
|
|
71
|
+
finally:
|
|
72
|
+
other.close()
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def test_the_flag_reflects_this_python():
|
|
76
|
+
assert sqlite_mod._HAS_BLOBOPEN == hasattr(sqlite3.Connection, "blobopen")
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def test_streams_the_exact_bytes_at_awkward_chunk_sizes(impl, zb):
|
|
80
|
+
data = _noise_jpeg()
|
|
81
|
+
image_id = zb.put(data)
|
|
82
|
+
for chunk_size in (1_000, 97, 4_096, len(data), len(data) + 5, 1_048_576):
|
|
83
|
+
chunks = list(zb.get_stream(image_id, chunk_size=chunk_size))
|
|
84
|
+
assert b"".join(chunks) == data, chunk_size
|
|
85
|
+
# every chunk but the last is exactly chunk_size, and none is empty
|
|
86
|
+
assert all(len(c) == chunk_size for c in chunks[:-1]), chunk_size
|
|
87
|
+
assert 0 < len(chunks[-1]) <= chunk_size, chunk_size
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def test_a_stream_in_progress_keeps_its_snapshot_when_the_row_is_deleted_elsewhere(impl, zb, path):
|
|
91
|
+
data = _noise_jpeg()
|
|
92
|
+
image_id = zb.put(data)
|
|
93
|
+
stream = zb.get_stream(image_id, chunk_size=500)
|
|
94
|
+
first = next(stream)
|
|
95
|
+
_delete_the_bytes_from_another_connection(path, image_id, dedup=zb._backend._dedup)
|
|
96
|
+
rest = b"".join(stream)
|
|
97
|
+
assert first + rest == data
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def test_a_row_deleted_before_the_first_chunk_raises_storage_error(impl, zb, path):
|
|
101
|
+
image_id = zb.put(_noise_jpeg())
|
|
102
|
+
stream = zb.get_stream(image_id, chunk_size=500) # nothing has been read yet
|
|
103
|
+
_delete_the_bytes_from_another_connection(path, image_id, dedup=zb._backend._dedup)
|
|
104
|
+
with pytest.raises(StorageError):
|
|
105
|
+
next(stream)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def test_an_open_stream_does_not_block_writers_in_other_connections(impl, zb, path):
|
|
109
|
+
image_id = zb.put(_noise_jpeg())
|
|
110
|
+
stream = zb.get_stream(image_id, chunk_size=500)
|
|
111
|
+
next(stream) # a read transaction is now open
|
|
112
|
+
other = ZeroBucket(backend=SQLiteBackend(path, dedup=zb._backend._dedup))
|
|
113
|
+
try:
|
|
114
|
+
# would raise "database is locked" if the open stream blocked writers
|
|
115
|
+
new_id = other.put(_noise_jpeg(64))
|
|
116
|
+
assert other.exists(new_id)
|
|
117
|
+
finally:
|
|
118
|
+
other.close()
|
|
119
|
+
stream.close()
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def test_the_read_transaction_is_released_after_completion_and_after_abandonment(impl, zb, path):
|
|
123
|
+
data = _noise_jpeg()
|
|
124
|
+
image_id = zb.put(data)
|
|
125
|
+
conn = sqlite3.connect(path)
|
|
126
|
+
try:
|
|
127
|
+
assert b"".join(zb.get_stream(image_id, chunk_size=700, connection=conn)) == data
|
|
128
|
+
assert conn.in_transaction is False, "finished stream left a transaction open"
|
|
129
|
+
|
|
130
|
+
stream = zb.get_stream(image_id, chunk_size=700, connection=conn)
|
|
131
|
+
next(stream)
|
|
132
|
+
stream.close() # abandoned half way
|
|
133
|
+
assert conn.in_transaction is False, "abandoned stream left a transaction open"
|
|
134
|
+
finally:
|
|
135
|
+
conn.close()
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def test_a_callers_own_open_transaction_is_used_and_left_open(impl, zb, path):
|
|
139
|
+
data = _noise_jpeg()
|
|
140
|
+
image_id = zb.put(data)
|
|
141
|
+
conn = sqlite3.connect(path)
|
|
142
|
+
try:
|
|
143
|
+
conn.execute("BEGIN")
|
|
144
|
+
assert conn.in_transaction
|
|
145
|
+
assert b"".join(zb.get_stream(image_id, chunk_size=700, connection=conn)) == data
|
|
146
|
+
assert conn.in_transaction, "the caller's transaction must not be ended by the stream"
|
|
147
|
+
conn.rollback()
|
|
148
|
+
finally:
|
|
149
|
+
conn.close()
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def test_two_references_to_one_dedup_blob_each_stream_correctly(impl, path):
|
|
153
|
+
zb = ZeroBucket(backend=SQLiteBackend(path, dedup=True))
|
|
154
|
+
try:
|
|
155
|
+
data = _noise_jpeg()
|
|
156
|
+
a, b = zb.put(data), zb.put(data)
|
|
157
|
+
assert b"".join(zb.get_stream(a, chunk_size=333)) == data
|
|
158
|
+
assert b"".join(zb.get_stream(b, chunk_size=1_000)) == data
|
|
159
|
+
finally:
|
|
160
|
+
zb.close()
|
|
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
|
|
File without changes
|
|
File without changes
|