flatfs 0.2.0__tar.gz → 0.4.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.
- {flatfs-0.2.0 → flatfs-0.4.0}/PKG-INFO +2 -2
- flatfs-0.4.0/flatfs/__init__.py +1 -0
- flatfs-0.4.0/flatfs/_compat.py +17 -0
- {flatfs-0.2.0 → flatfs-0.4.0}/flatfs/_utils.py +53 -1
- {flatfs-0.2.0 → flatfs-0.4.0}/flatfs/backends.py +29 -26
- flatfs-0.4.0/flatfs/helpers.py +436 -0
- {flatfs-0.2.0 → flatfs-0.4.0}/flatfs/interface.py +56 -53
- {flatfs-0.2.0 → flatfs-0.4.0}/pyproject.toml +5 -5
- flatfs-0.2.0/flatfs/__init__.py +0 -1
- flatfs-0.2.0/flatfs/helpers.py +0 -158
- {flatfs-0.2.0 → flatfs-0.4.0}/LICENSE.txt +0 -0
- {flatfs-0.2.0 → flatfs-0.4.0}/README.md +0 -0
- {flatfs-0.2.0 → flatfs-0.4.0}/flatfs/_export.py +0 -0
- {flatfs-0.2.0 → flatfs-0.4.0}/flatfs/api.py +0 -0
- {flatfs-0.2.0 → flatfs-0.4.0}/flatfs/exc.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flatfs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: Minimal filesystem abstraction with pluggable backends, treating paths as keys and file contents as values.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE.txt
|
|
7
7
|
Keywords: flat,filesystem,abstraction,library
|
|
8
8
|
Author: Maciej Wiatrzyk
|
|
9
9
|
Author-email: maciej.wiatrzyk@gmail.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9,<4
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.4.0"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any, Coroutine, Generic, Optional, Protocol, TypeVar
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
Tco = TypeVar("Tco", covariant=True)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SupportsAnext(Protocol, Generic[Tco]):
|
|
8
|
+
def __anext__(self) -> Coroutine[Any, Any, Tco]: ...
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def anext(obj: SupportsAnext[T], default: Optional[T] = None) -> T:
|
|
12
|
+
try:
|
|
13
|
+
return await obj.__anext__()
|
|
14
|
+
except StopAsyncIteration:
|
|
15
|
+
if default is not None:
|
|
16
|
+
return default
|
|
17
|
+
raise
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import datetime
|
|
3
3
|
import functools
|
|
4
|
-
|
|
4
|
+
import queue
|
|
5
|
+
from typing import AsyncGenerator, Callable, Generator, TypeVar
|
|
5
6
|
|
|
6
7
|
from flatfs.exc import PathAccessError
|
|
7
8
|
|
|
@@ -56,3 +57,54 @@ async def run_blocking(func: Callable[..., T], *args, **kwargs) -> T:
|
|
|
56
57
|
def utcnow() -> datetime.datetime:
|
|
57
58
|
"""Get the current date and time in UTC."""
|
|
58
59
|
return datetime.datetime.now(datetime.timezone.utc)
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def split_into_chunks(data: bytes, chunk_size: int) -> Generator[bytes, None, None]:
|
|
63
|
+
"""Generator that splits given binary data into chunks of provided maximum size.
|
|
64
|
+
|
|
65
|
+
:param data:
|
|
66
|
+
The data to split.
|
|
67
|
+
|
|
68
|
+
:chunk_size:
|
|
69
|
+
The maximum size of a single chunk.
|
|
70
|
+
"""
|
|
71
|
+
while chunk := data[:chunk_size]:
|
|
72
|
+
yield chunk
|
|
73
|
+
data = data[chunk_size:]
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
async def async_split_into_chunks(data: bytes, chunk_size: int) -> AsyncGenerator[bytes, None]:
|
|
77
|
+
"""Async variant of the :func:`split_into_chunks` function.
|
|
78
|
+
|
|
79
|
+
:param data:
|
|
80
|
+
The data to split.
|
|
81
|
+
|
|
82
|
+
:param chunk_size:
|
|
83
|
+
The maximum size of a single chunk.
|
|
84
|
+
"""
|
|
85
|
+
while chunk := data[:chunk_size]:
|
|
86
|
+
yield chunk
|
|
87
|
+
data = data[chunk_size:]
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def generate_chunks_from_queue(chunk_queue: queue.Queue[bytes]) -> Generator[bytes, None, None]:
|
|
91
|
+
"""Generator that yields chunks it receives via provided queue until empty
|
|
92
|
+
bytes object is received.
|
|
93
|
+
|
|
94
|
+
:param chunk_queue:
|
|
95
|
+
Chunk queue to adapt.
|
|
96
|
+
"""
|
|
97
|
+
while True:
|
|
98
|
+
chunk = chunk_queue.get()
|
|
99
|
+
if not chunk:
|
|
100
|
+
return
|
|
101
|
+
yield chunk
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def async_generate_chunks_from_queue(chunk_queue: asyncio.Queue[bytes]) -> AsyncGenerator[bytes, None]:
|
|
105
|
+
"""Async variant of the :func:`generate_chunks_from_queue` function."""
|
|
106
|
+
while True:
|
|
107
|
+
chunk = await chunk_queue.get()
|
|
108
|
+
if not chunk:
|
|
109
|
+
return
|
|
110
|
+
yield chunk
|
|
@@ -5,7 +5,7 @@ import fnmatch
|
|
|
5
5
|
import os
|
|
6
6
|
import pathlib
|
|
7
7
|
from queue import Queue
|
|
8
|
-
from typing import AsyncGenerator,
|
|
8
|
+
from typing import AsyncGenerator, AsyncIterator, Generator, Iterator, Optional, Union
|
|
9
9
|
|
|
10
10
|
from flatfs.exc import PathAccessError, PathNotFoundError
|
|
11
11
|
from flatfs.interface import FlatFsReaderWriter, Stat
|
|
@@ -107,14 +107,12 @@ class LocalFlatFs:
|
|
|
107
107
|
break
|
|
108
108
|
yield chunk
|
|
109
109
|
|
|
110
|
-
def write_chunks(self, path: str,
|
|
110
|
+
def write_chunks(self, path: str, chunk_gen: Generator[bytes, None, None]) -> Generator[int, None, None]:
|
|
111
111
|
abspath = self.__make_abspath(path)
|
|
112
112
|
abspath.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
-
total_bytes = 0
|
|
114
113
|
with abspath.open("wb") as fd:
|
|
115
|
-
for chunk in
|
|
116
|
-
|
|
117
|
-
return total_bytes
|
|
114
|
+
for chunk in chunk_gen:
|
|
115
|
+
yield fd.write(chunk)
|
|
118
116
|
|
|
119
117
|
def remove(self, path: str):
|
|
120
118
|
abspath = self.__make_abspath(path)
|
|
@@ -165,15 +163,18 @@ class InMemoryFlatFs:
|
|
|
165
163
|
data_left = data_left[chunk_size:]
|
|
166
164
|
yield chunk
|
|
167
165
|
|
|
168
|
-
def write_chunks(self, path: str,
|
|
169
|
-
payload = b"".join(chunks)
|
|
166
|
+
def write_chunks(self, path: str, chunk_gen: Generator[bytes, None, None]) -> Generator[int, None, None]:
|
|
170
167
|
now = _utils.utcnow()
|
|
171
168
|
stat = Stat(
|
|
172
169
|
modified=now,
|
|
173
|
-
size=
|
|
170
|
+
size=0,
|
|
174
171
|
)
|
|
175
|
-
self.__storage[self.__make_key(path)] = self._File(
|
|
176
|
-
|
|
172
|
+
self.__storage[self.__make_key(path)] = file = self._File(b"", stat)
|
|
173
|
+
for chunk in chunk_gen:
|
|
174
|
+
file.payload += chunk
|
|
175
|
+
write_count = len(chunk)
|
|
176
|
+
file.stat.size += write_count
|
|
177
|
+
yield write_count
|
|
177
178
|
|
|
178
179
|
def remove(self, path: str):
|
|
179
180
|
key = self.__make_key(path)
|
|
@@ -221,7 +222,7 @@ class AsyncFlatFsAdapter:
|
|
|
221
222
|
async def stat(self, path: str) -> Stat:
|
|
222
223
|
return await _utils.run_blocking(self.__target.stat, path)
|
|
223
224
|
|
|
224
|
-
async def read_chunks(self, path: str, chunk_size: int = 65535) ->
|
|
225
|
+
async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncIterator[bytes]:
|
|
225
226
|
|
|
226
227
|
def reader():
|
|
227
228
|
try:
|
|
@@ -232,7 +233,7 @@ class AsyncFlatFsAdapter:
|
|
|
232
233
|
loop.call_soon_threadsafe(queue.put_nowait, e)
|
|
233
234
|
|
|
234
235
|
loop = asyncio.get_running_loop()
|
|
235
|
-
queue: asyncio.Queue[bytes
|
|
236
|
+
queue: asyncio.Queue[Union[bytes, Exception]] = asyncio.Queue()
|
|
236
237
|
asyncio.create_task(_utils.run_blocking(reader))
|
|
237
238
|
while True:
|
|
238
239
|
chunk_or_exc = await queue.get()
|
|
@@ -242,21 +243,23 @@ class AsyncFlatFsAdapter:
|
|
|
242
243
|
raise chunk_or_exc
|
|
243
244
|
yield chunk_or_exc
|
|
244
245
|
|
|
245
|
-
async def write_chunks(self, path: str,
|
|
246
|
+
async def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
|
|
246
247
|
|
|
247
|
-
def
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
if not chunk:
|
|
251
|
-
return
|
|
252
|
-
yield chunk
|
|
248
|
+
def blocking_writer():
|
|
249
|
+
for write_count in self.__target.write_chunks(path, _utils.generate_chunks_from_queue(chunk_queue)):
|
|
250
|
+
loop.call_soon_threadsafe(ack_queue.put_nowait, write_count)
|
|
253
251
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
252
|
+
loop = asyncio.get_running_loop()
|
|
253
|
+
chunk_queue: Queue[bytes] = Queue()
|
|
254
|
+
ack_queue: asyncio.Queue[int] = asyncio.Queue()
|
|
255
|
+
task = asyncio.create_task(_utils.run_blocking(blocking_writer))
|
|
256
|
+
try:
|
|
257
|
+
async for chunk in chunk_gen:
|
|
258
|
+
chunk_queue.put(chunk)
|
|
259
|
+
yield await ack_queue.get()
|
|
260
|
+
finally:
|
|
261
|
+
chunk_queue.put(b"")
|
|
262
|
+
await task
|
|
260
263
|
|
|
261
264
|
async def remove(self, path: str):
|
|
262
265
|
await _utils.run_blocking(self.__target.remove, path)
|
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
|
3
|
+
from queue import Queue
|
|
4
|
+
from typing import Iterator, AsyncIterator, Optional
|
|
5
|
+
|
|
6
|
+
from . import _export, _utils, _compat
|
|
7
|
+
from .interface import SupportsAsyncReadChunks, SupportsAsyncWriteChunks, SupportsReadChunks, SupportsWriteChunks
|
|
8
|
+
|
|
9
|
+
__all__ = export = _export.Export() # type: ignore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@export
|
|
13
|
+
def read_bytes(fs: SupportsReadChunks, path: str, chunk_size: int = 65535) -> bytes:
|
|
14
|
+
"""Read whole file and return as bytes.
|
|
15
|
+
|
|
16
|
+
:param fs:
|
|
17
|
+
FlatFS object to read from.
|
|
18
|
+
|
|
19
|
+
:param path:
|
|
20
|
+
Path to a file to read.
|
|
21
|
+
|
|
22
|
+
:param chunk_size:
|
|
23
|
+
The chunk size to use during read.
|
|
24
|
+
|
|
25
|
+
This is the maximum number of bytes to read from a file per single read
|
|
26
|
+
operation.
|
|
27
|
+
|
|
28
|
+
Defaults to 64kB.
|
|
29
|
+
"""
|
|
30
|
+
out = b""
|
|
31
|
+
for chunk in fs.read_chunks(path, chunk_size):
|
|
32
|
+
out += chunk
|
|
33
|
+
return out
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@export
|
|
37
|
+
async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: int = 65535) -> bytes:
|
|
38
|
+
"""Async variant of the :func:`read_bytes` helper."""
|
|
39
|
+
out = b""
|
|
40
|
+
async for chunk in fs.read_chunks(path, chunk_size):
|
|
41
|
+
out += chunk
|
|
42
|
+
return out
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@export
|
|
46
|
+
def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
47
|
+
"""Write given data to file at given path either creating the file, or
|
|
48
|
+
replacing with new content.
|
|
49
|
+
|
|
50
|
+
Returns the number of bytes written.
|
|
51
|
+
|
|
52
|
+
:param fs:
|
|
53
|
+
FlatFS object to write to.
|
|
54
|
+
|
|
55
|
+
:param path:
|
|
56
|
+
The path to a file to create or replace.
|
|
57
|
+
|
|
58
|
+
:param data:
|
|
59
|
+
The data to write.
|
|
60
|
+
|
|
61
|
+
:param chunk_size:
|
|
62
|
+
The chunk size to use during writing.
|
|
63
|
+
|
|
64
|
+
If *data* length is greater, it will be split into chunks and written
|
|
65
|
+
chunk by chunk, with each chunk having at most *chunk_size* bytes.
|
|
66
|
+
|
|
67
|
+
Defaults to 64kB.
|
|
68
|
+
"""
|
|
69
|
+
total_bytes = 0
|
|
70
|
+
for byte_count in fs.write_chunks(path, _utils.split_into_chunks(data, chunk_size)):
|
|
71
|
+
total_bytes += byte_count
|
|
72
|
+
return total_bytes
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
@export
|
|
76
|
+
async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
77
|
+
"""Async variant of the :func:`write_bytes` helper."""
|
|
78
|
+
total_bytes = 0
|
|
79
|
+
async for byte_count in fs.write_chunks(path, _utils.async_split_into_chunks(data, chunk_size)):
|
|
80
|
+
total_bytes += byte_count
|
|
81
|
+
return total_bytes
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
@export
|
|
85
|
+
def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535) -> str:
|
|
86
|
+
"""Read whole text file and return as string.
|
|
87
|
+
|
|
88
|
+
Uses :func:`read_bytes` underneath and then just decodes its output using
|
|
89
|
+
provided *encoding*, which is ``utf-8`` by default.
|
|
90
|
+
|
|
91
|
+
:param fs:
|
|
92
|
+
FlatFS object to read from.
|
|
93
|
+
|
|
94
|
+
:param path:
|
|
95
|
+
The path to a file to read.
|
|
96
|
+
|
|
97
|
+
:param encoding:
|
|
98
|
+
The encoding to use.
|
|
99
|
+
|
|
100
|
+
:param chunk_size:
|
|
101
|
+
The chunk size to use during reading.
|
|
102
|
+
|
|
103
|
+
This tells how many bytes can at most be read in single pass.
|
|
104
|
+
"""
|
|
105
|
+
return read_bytes(fs, path, chunk_size).decode(encoding)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@export
|
|
109
|
+
async def async_read_text(
|
|
110
|
+
fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
111
|
+
) -> str:
|
|
112
|
+
"""Async variant of the :func:`read_text` helper."""
|
|
113
|
+
return (await async_read_bytes(fs, path, chunk_size)).decode(encoding)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
@export
|
|
117
|
+
def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535):
|
|
118
|
+
"""Create new text file or replace existing with given text data.
|
|
119
|
+
|
|
120
|
+
Uses :func:`write_bytes` underneath and just encodes given *data* with
|
|
121
|
+
provided *encoding*, which is ``utf-8`` by default.
|
|
122
|
+
|
|
123
|
+
:param fs:
|
|
124
|
+
FlatFS object to write to.
|
|
125
|
+
|
|
126
|
+
:param path:
|
|
127
|
+
The path to a file to create or replace.
|
|
128
|
+
|
|
129
|
+
:param encoding:
|
|
130
|
+
The encoding to use.
|
|
131
|
+
|
|
132
|
+
:param chunk_size:
|
|
133
|
+
The chunk size to use during writing.
|
|
134
|
+
|
|
135
|
+
This tells how many bytes can at most be written in a single pass.
|
|
136
|
+
"""
|
|
137
|
+
return write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
@export
|
|
141
|
+
async def async_write_text(
|
|
142
|
+
fs: SupportsAsyncWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
143
|
+
):
|
|
144
|
+
"""Async variant of the :func:`write_text` helper."""
|
|
145
|
+
await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@export
|
|
149
|
+
class BufferedWriter(AbstractContextManager):
|
|
150
|
+
"""Buffered writer for creating new or replacing existing files.
|
|
151
|
+
|
|
152
|
+
It allows efficient writing of arbitrary binary data thanks to the buffer
|
|
153
|
+
being used. Once the length of buffered data exceeds given buffer size, the
|
|
154
|
+
data is flushed to the underlying filesystem and buffer is cleared. This
|
|
155
|
+
allows to limit actual write operations to minimum.
|
|
156
|
+
|
|
157
|
+
This helper can be used as a context manager.
|
|
158
|
+
|
|
159
|
+
:param fs:
|
|
160
|
+
FlatFS object to write to.
|
|
161
|
+
|
|
162
|
+
:param path:
|
|
163
|
+
Path to a file to create or replace.
|
|
164
|
+
|
|
165
|
+
:param buffer_size:
|
|
166
|
+
The size of a buffer, in bytes.
|
|
167
|
+
|
|
168
|
+
This is the maximum amount of data that can be buffered before it is
|
|
169
|
+
flushed to the backend. This is equivalent to the maximum size of a
|
|
170
|
+
single chunk that is written to the underlying filesystem.
|
|
171
|
+
"""
|
|
172
|
+
|
|
173
|
+
def __init__(self, fs: SupportsWriteChunks, path: str, buffer_size: int = 65535):
|
|
174
|
+
self.__chunk_queue: Queue[bytes] = Queue()
|
|
175
|
+
self.__buffer_size = buffer_size
|
|
176
|
+
self.__buffer = b""
|
|
177
|
+
self.__writer = fs.write_chunks(path, _utils.generate_chunks_from_queue(self.__chunk_queue))
|
|
178
|
+
|
|
179
|
+
def __enter__(self) -> "BufferedWriter":
|
|
180
|
+
return self
|
|
181
|
+
|
|
182
|
+
def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
183
|
+
return self.close()
|
|
184
|
+
|
|
185
|
+
def __write_chunk(self, chunk: bytes):
|
|
186
|
+
self.__chunk_queue.put(chunk)
|
|
187
|
+
next(self.__writer)
|
|
188
|
+
|
|
189
|
+
def flush(self):
|
|
190
|
+
"""Flush the buffer and clear it.
|
|
191
|
+
|
|
192
|
+
This is automatically called on close or context manager exit.
|
|
193
|
+
"""
|
|
194
|
+
if not self.__buffer:
|
|
195
|
+
return # Nothing to do
|
|
196
|
+
self.__write_chunk(self.__buffer)
|
|
197
|
+
self.__buffer = b""
|
|
198
|
+
|
|
199
|
+
def close(self):
|
|
200
|
+
"""Flush remaining data and close underlying file."""
|
|
201
|
+
self.flush()
|
|
202
|
+
return self.__writer.close()
|
|
203
|
+
|
|
204
|
+
def write(self, data: bytes) -> int:
|
|
205
|
+
"""Write data to the buffer.
|
|
206
|
+
|
|
207
|
+
If the buffer is full, it will be flushed and remaining data (if any)
|
|
208
|
+
will be used to initialize emptied buffer.
|
|
209
|
+
|
|
210
|
+
:param data:
|
|
211
|
+
The data to write.
|
|
212
|
+
"""
|
|
213
|
+
self.__buffer += data
|
|
214
|
+
while len(self.__buffer) > self.__buffer_size:
|
|
215
|
+
chunk = self.__buffer[: self.__buffer_size]
|
|
216
|
+
self.__write_chunk(chunk)
|
|
217
|
+
self.__buffer = self.__buffer[self.__buffer_size :]
|
|
218
|
+
return len(data)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@export
|
|
222
|
+
class AsyncBufferedWriter(AbstractAsyncContextManager):
|
|
223
|
+
"""Async variant of the :class:`BufferedWriter` class."""
|
|
224
|
+
|
|
225
|
+
def __init__(self, fs: SupportsAsyncWriteChunks, path: str, buffer_size: int = 65535):
|
|
226
|
+
self.__chunk_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
227
|
+
self.__buffer_size = buffer_size
|
|
228
|
+
self.__buffer = b""
|
|
229
|
+
self.__writer = fs.write_chunks(path, _utils.async_generate_chunks_from_queue(self.__chunk_queue))
|
|
230
|
+
|
|
231
|
+
async def __aenter__(self) -> "AsyncBufferedWriter":
|
|
232
|
+
return self
|
|
233
|
+
|
|
234
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
235
|
+
return await self.close()
|
|
236
|
+
|
|
237
|
+
async def __write_chunk(self, chunk: bytes):
|
|
238
|
+
self.__chunk_queue.put_nowait(chunk)
|
|
239
|
+
await _compat.anext(self.__writer)
|
|
240
|
+
|
|
241
|
+
async def write(self, data: bytes) -> int:
|
|
242
|
+
self.__buffer += data
|
|
243
|
+
while len(self.__buffer) > self.__buffer_size:
|
|
244
|
+
chunk = self.__buffer[: self.__buffer_size]
|
|
245
|
+
await self.__write_chunk(chunk)
|
|
246
|
+
self.__buffer = self.__buffer[self.__buffer_size :]
|
|
247
|
+
return len(data)
|
|
248
|
+
|
|
249
|
+
async def flush(self):
|
|
250
|
+
if not self.__buffer:
|
|
251
|
+
return # Nothing to do
|
|
252
|
+
await self.__write_chunk(self.__buffer)
|
|
253
|
+
self.__buffer = b""
|
|
254
|
+
|
|
255
|
+
async def close(self):
|
|
256
|
+
await self.flush()
|
|
257
|
+
await self.__writer.aclose()
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
@export
|
|
261
|
+
class BufferedReader(AbstractContextManager):
|
|
262
|
+
"""Helper for buffered reading from underlying flat filesystem.
|
|
263
|
+
|
|
264
|
+
If reads from an underlying filesystem chunks of bytes of given maximum
|
|
265
|
+
length and stores it locally for reading. When entire buffer is read, the
|
|
266
|
+
next chunk is fetched from the filesystem. The whole operation is repeated
|
|
267
|
+
until file is fully read or the reader is closed.
|
|
268
|
+
|
|
269
|
+
This helper can be used via context manager.
|
|
270
|
+
|
|
271
|
+
:param fs:
|
|
272
|
+
The filesystem to read from.
|
|
273
|
+
|
|
274
|
+
:param path:
|
|
275
|
+
The path to a file to read.
|
|
276
|
+
|
|
277
|
+
:param buffer_size:
|
|
278
|
+
The size of a read buffer, in bytes.
|
|
279
|
+
|
|
280
|
+
This is also the maximum size of a single chunk read from a file.
|
|
281
|
+
"""
|
|
282
|
+
|
|
283
|
+
def __init__(self, fs: SupportsReadChunks, path: str, buffer_size: int = 65535):
|
|
284
|
+
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
285
|
+
self.__buffer = b""
|
|
286
|
+
|
|
287
|
+
def __enter__(self) -> "BufferedReader":
|
|
288
|
+
return self
|
|
289
|
+
|
|
290
|
+
def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
291
|
+
return self.close()
|
|
292
|
+
|
|
293
|
+
def close(self):
|
|
294
|
+
self.__buffer = b""
|
|
295
|
+
self.__reader.close()
|
|
296
|
+
|
|
297
|
+
def read(self, count: int) -> bytes:
|
|
298
|
+
if not self.__buffer:
|
|
299
|
+
self.__buffer = next(self.__reader, b"")
|
|
300
|
+
data = self.__buffer[:count]
|
|
301
|
+
self.__buffer = self.__buffer[count:]
|
|
302
|
+
return data
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
@export
|
|
306
|
+
class AsyncBufferedReader(AbstractAsyncContextManager):
|
|
307
|
+
"""Async variant of the :class:`BufferedReader` class."""
|
|
308
|
+
|
|
309
|
+
def __init__(self, fs: SupportsAsyncReadChunks, path: str, buffer_size: int = 65535):
|
|
310
|
+
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
311
|
+
self.__buffer = b""
|
|
312
|
+
|
|
313
|
+
async def __aenter__(self) -> "AsyncBufferedReader":
|
|
314
|
+
return self
|
|
315
|
+
|
|
316
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
317
|
+
return await self.close()
|
|
318
|
+
|
|
319
|
+
async def close(self):
|
|
320
|
+
self.__buffer = b""
|
|
321
|
+
await self.__reader.aclose()
|
|
322
|
+
|
|
323
|
+
async def read(self, count: int) -> bytes:
|
|
324
|
+
if not self.__buffer:
|
|
325
|
+
self.__buffer = await _compat.anext(self.__reader, b"")
|
|
326
|
+
data = self.__buffer[:count]
|
|
327
|
+
self.__buffer = self.__buffer[count:]
|
|
328
|
+
return data
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
@export
|
|
332
|
+
class BufferedTextReader(AbstractContextManager):
|
|
333
|
+
"""Equivalent of :class:`BufferedReader` for text files.
|
|
334
|
+
|
|
335
|
+
It reads at most *buffer_size* bytes from underlying file, stores in local
|
|
336
|
+
buffer, and then streams from that buffer until it expires. Once it is
|
|
337
|
+
expired and more data is needed, the next chunk of data is automatically
|
|
338
|
+
read and stored in the buffer.
|
|
339
|
+
|
|
340
|
+
:param fs:
|
|
341
|
+
The filesystem reader object.
|
|
342
|
+
|
|
343
|
+
:param path:
|
|
344
|
+
The path to a text file to read.
|
|
345
|
+
|
|
346
|
+
:param encoding:
|
|
347
|
+
The encoding to use.
|
|
348
|
+
|
|
349
|
+
:param buffer_size:
|
|
350
|
+
The maximum amount of data to read from an underlying file in one pass,
|
|
351
|
+
in bytes.
|
|
352
|
+
"""
|
|
353
|
+
|
|
354
|
+
def __init__(self, fs: SupportsReadChunks, path: str, encoding: str = "utf-8", buffer_size: int = 65535):
|
|
355
|
+
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
356
|
+
self.__encoding = encoding
|
|
357
|
+
self.__buffer = ""
|
|
358
|
+
|
|
359
|
+
def __read_next_chunk(self) -> str:
|
|
360
|
+
return next(self.__reader, b"").decode(self.__encoding)
|
|
361
|
+
|
|
362
|
+
def __enter__(self) -> "BufferedTextReader":
|
|
363
|
+
return self
|
|
364
|
+
|
|
365
|
+
def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
366
|
+
return self.close()
|
|
367
|
+
|
|
368
|
+
def __iter__(self) -> Iterator[str]:
|
|
369
|
+
while next_chunk := self.__read_next_chunk():
|
|
370
|
+
self.__buffer += next_chunk
|
|
371
|
+
lines = self.__buffer.splitlines()
|
|
372
|
+
if len(lines) > 1:
|
|
373
|
+
for line in lines[:-1]:
|
|
374
|
+
yield line
|
|
375
|
+
self.__buffer = lines[-1]
|
|
376
|
+
yield self.__buffer
|
|
377
|
+
|
|
378
|
+
def read(self, count: int) -> str:
|
|
379
|
+
"""Read at most *count* characters.
|
|
380
|
+
|
|
381
|
+
:param count:
|
|
382
|
+
The maximum number of characters to read.
|
|
383
|
+
"""
|
|
384
|
+
if not self.__buffer:
|
|
385
|
+
self.__buffer = self.__read_next_chunk()
|
|
386
|
+
out = self.__buffer[:count]
|
|
387
|
+
self.__buffer = self.__buffer[count:]
|
|
388
|
+
return out
|
|
389
|
+
|
|
390
|
+
def close(self):
|
|
391
|
+
"""Close this reader.
|
|
392
|
+
|
|
393
|
+
This also closes the underlying file.
|
|
394
|
+
"""
|
|
395
|
+
self.__buffer = ""
|
|
396
|
+
self.__reader.close()
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
@export
|
|
400
|
+
class AsyncBufferedTextReader(AbstractAsyncContextManager):
|
|
401
|
+
"""Async variant of the :class:`BufferedTextReader` class."""
|
|
402
|
+
|
|
403
|
+
def __init__(self, fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", buffer_size: int = 65535):
|
|
404
|
+
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
405
|
+
self.__encoding = encoding
|
|
406
|
+
self.__buffer = ""
|
|
407
|
+
|
|
408
|
+
async def __read_next_chunk(self) -> str:
|
|
409
|
+
return (await _compat.anext(self.__reader, b"")).decode(self.__encoding)
|
|
410
|
+
|
|
411
|
+
async def __aenter__(self) -> "AsyncBufferedTextReader":
|
|
412
|
+
return self
|
|
413
|
+
|
|
414
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
415
|
+
return await self.close()
|
|
416
|
+
|
|
417
|
+
async def __aiter__(self) -> AsyncIterator[str]:
|
|
418
|
+
while next_chunk := await self.__read_next_chunk():
|
|
419
|
+
self.__buffer += next_chunk
|
|
420
|
+
lines = self.__buffer.splitlines()
|
|
421
|
+
if len(lines) > 1:
|
|
422
|
+
for line in lines[:-1]:
|
|
423
|
+
yield line
|
|
424
|
+
self.__buffer = lines[-1]
|
|
425
|
+
yield self.__buffer
|
|
426
|
+
|
|
427
|
+
async def read(self, count: int) -> str:
|
|
428
|
+
if not self.__buffer:
|
|
429
|
+
self.__buffer = await self.__read_next_chunk()
|
|
430
|
+
out = self.__buffer[:count]
|
|
431
|
+
self.__buffer = self.__buffer[count:]
|
|
432
|
+
return out
|
|
433
|
+
|
|
434
|
+
async def close(self):
|
|
435
|
+
self.__buffer = ""
|
|
436
|
+
await self.__reader.aclose()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import datetime
|
|
3
|
-
from typing import AsyncGenerator,
|
|
3
|
+
from typing import AsyncGenerator, AsyncIterator, Generator, Iterator, Protocol
|
|
4
4
|
|
|
5
5
|
from . import _export
|
|
6
6
|
|
|
@@ -24,14 +24,15 @@ class SupportsReadChunks(Protocol):
|
|
|
24
24
|
"""Protocol for objects that support chunked reading."""
|
|
25
25
|
|
|
26
26
|
def read_chunks(self, path: str, chunk_size: int = 65535) -> Generator[bytes, None, None]:
|
|
27
|
-
"""
|
|
27
|
+
"""Return generator for reading existing file in chunks of given
|
|
28
|
+
maximum size.
|
|
28
29
|
|
|
29
|
-
|
|
30
|
-
getting out of memory.
|
|
31
|
-
|
|
32
|
-
If *path* does not point to an existing file,
|
|
30
|
+
If *path* does not point to an existing file
|
|
33
31
|
:exc:`flatfs.exc.PathNotFoundError` will be raised.
|
|
34
32
|
|
|
33
|
+
Closing or expiring the returned generator automatically closes
|
|
34
|
+
underlying file.
|
|
35
|
+
|
|
35
36
|
:param path:
|
|
36
37
|
The path to a file to read.
|
|
37
38
|
|
|
@@ -47,18 +48,21 @@ class SupportsReadChunks(Protocol):
|
|
|
47
48
|
class SupportsWriteChunks(Protocol):
|
|
48
49
|
"""Protocol for object that support writing in chunks."""
|
|
49
50
|
|
|
50
|
-
def write_chunks(self, path: str,
|
|
51
|
-
"""Create or
|
|
52
|
-
|
|
51
|
+
def write_chunks(self, path: str, chunk_gen: Generator[bytes, None, None]) -> Generator[int, None, None]:
|
|
52
|
+
"""Create or replace file at given path.
|
|
53
|
+
|
|
54
|
+
Returns a generator that consumes a chunk from provided *chunk_gen*,
|
|
55
|
+
writes it to the file, and yields number of bytes written for the
|
|
56
|
+
current chunk.
|
|
53
57
|
|
|
54
|
-
|
|
55
|
-
|
|
58
|
+
Closing or expiring returned generator automatically closes underlying
|
|
59
|
+
file and flushes what was written so far.
|
|
56
60
|
|
|
57
61
|
:param path:
|
|
58
|
-
The path to a file
|
|
62
|
+
The path to a file.
|
|
59
63
|
|
|
60
|
-
:param
|
|
61
|
-
|
|
64
|
+
:param chunk_gen:
|
|
65
|
+
Generator producing chunks to be written.
|
|
62
66
|
"""
|
|
63
67
|
...
|
|
64
68
|
|
|
@@ -68,15 +72,13 @@ class FlatFsReader(SupportsReadChunks, Protocol):
|
|
|
68
72
|
"""Read-only interface to access files in the flat filesystem."""
|
|
69
73
|
|
|
70
74
|
def scan(self) -> Iterator[str]:
|
|
71
|
-
"""
|
|
72
|
-
|
|
73
|
-
Each yielded path can later be used with other methods of this
|
|
74
|
-
interface.
|
|
75
|
-
"""
|
|
75
|
+
"""Return iterator that yields normalized paths to existing files."""
|
|
76
76
|
...
|
|
77
77
|
|
|
78
78
|
def stat(self, path: str) -> Stat:
|
|
79
|
-
"""Get stats of a file at given
|
|
79
|
+
"""Get stats of a file at given path.
|
|
80
|
+
|
|
81
|
+
This can be used to retrieve file size, file modification time etc.
|
|
80
82
|
|
|
81
83
|
If the file does not exist, :exc:`flatfs.exc.PathNotFoundError` will be
|
|
82
84
|
raised.
|
|
@@ -87,7 +89,7 @@ class FlatFsReader(SupportsReadChunks, Protocol):
|
|
|
87
89
|
...
|
|
88
90
|
|
|
89
91
|
def exists(self, path: str) -> bool:
|
|
90
|
-
"""Check if
|
|
92
|
+
"""Check if path points to an existing file.
|
|
91
93
|
|
|
92
94
|
:param path:
|
|
93
95
|
The path to a file.
|
|
@@ -100,10 +102,10 @@ class FlatFsReaderWriter(FlatFsReader, SupportsWriteChunks, Protocol):
|
|
|
100
102
|
"""Read-write interface to access files in the flat filesystem."""
|
|
101
103
|
|
|
102
104
|
def remove(self, path: str):
|
|
103
|
-
"""Remove file at given
|
|
105
|
+
"""Remove existing file at given path.
|
|
104
106
|
|
|
105
107
|
Raises :exc:`flatfs.exc.PathNotFoundError` if there was no file to
|
|
106
|
-
remove at given
|
|
108
|
+
remove at given path.
|
|
107
109
|
|
|
108
110
|
:param path:
|
|
109
111
|
The path to a file to remove.
|
|
@@ -113,61 +115,57 @@ class FlatFsReaderWriter(FlatFsReader, SupportsWriteChunks, Protocol):
|
|
|
113
115
|
|
|
114
116
|
@export
|
|
115
117
|
class SupportsAsyncReadChunks(Protocol):
|
|
118
|
+
"""Async variant of the :class:`SupportsReadChunks` protocol."""
|
|
119
|
+
|
|
116
120
|
def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
|
|
117
|
-
"""
|
|
121
|
+
"""Return a generator for reading file in chunks of given maximum size.
|
|
118
122
|
|
|
119
|
-
This
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
If *path* does not point to an existing file,
|
|
123
|
-
:exc:`flatfs.exc.PathNotFoundError` will be raised.
|
|
123
|
+
This is the async version of :meth:`SupportsReadChunks.read_chunks`
|
|
124
|
+
method.
|
|
124
125
|
|
|
125
126
|
:param path:
|
|
126
127
|
The path to a file to read.
|
|
127
128
|
|
|
128
129
|
:param chunk_size:
|
|
129
|
-
The maximum chunk size
|
|
130
|
-
|
|
131
|
-
This is the maximum length of a single generated bytes object.
|
|
130
|
+
The maximum chunk size, in bytes.
|
|
132
131
|
"""
|
|
133
132
|
...
|
|
134
133
|
|
|
135
134
|
|
|
136
135
|
@export
|
|
137
136
|
class SupportsAsyncWriteChunks(Protocol):
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
137
|
+
"""Async variant of the :class:`SupportsWriteChunks` protocol."""
|
|
138
|
+
|
|
139
|
+
def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
|
|
140
|
+
"""Generator for creating or overwriting files.
|
|
141
141
|
|
|
142
|
-
|
|
143
|
-
|
|
142
|
+
This is the async version of :meth:`SupportsWriteChunks.write_chunks`
|
|
143
|
+
method.
|
|
144
144
|
|
|
145
145
|
:param path:
|
|
146
|
-
The path to a file to create or
|
|
146
|
+
The path to a file to create or replace.
|
|
147
147
|
|
|
148
|
-
:param
|
|
149
|
-
|
|
148
|
+
:param chunk_gen:
|
|
149
|
+
Async generator yielding file chunks.
|
|
150
150
|
"""
|
|
151
151
|
...
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
@export
|
|
155
155
|
class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
|
|
156
|
-
"""Async
|
|
156
|
+
"""Async variant of the :class:`FlatFsReader` protocol."""
|
|
157
157
|
|
|
158
|
-
def scan(self) ->
|
|
159
|
-
"""
|
|
158
|
+
def scan(self) -> AsyncIterator[str]:
|
|
159
|
+
"""Return iterator that yields normalized paths to existing files.
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
interface.
|
|
161
|
+
This is the async version of :meth:`FlatFsReader.scan` method.
|
|
163
162
|
"""
|
|
164
163
|
...
|
|
165
164
|
|
|
166
165
|
async def stat(self, path: str) -> Stat:
|
|
167
|
-
"""Get stats of a file at given
|
|
166
|
+
"""Get stats of a file at given path.
|
|
168
167
|
|
|
169
|
-
|
|
170
|
-
raised.
|
|
168
|
+
This is the async version of :meth:`FlatFsReader.stat` method.
|
|
171
169
|
|
|
172
170
|
:param path:
|
|
173
171
|
The path to a file.
|
|
@@ -175,19 +173,24 @@ class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
|
|
|
175
173
|
...
|
|
176
174
|
|
|
177
175
|
async def exists(self, path: str) -> bool:
|
|
178
|
-
"""Check if
|
|
176
|
+
"""Check if file at given path exists.
|
|
177
|
+
|
|
178
|
+
This is the async version of :meth:`FlatFsReader.exists` method.
|
|
179
|
+
|
|
180
|
+
:param path:
|
|
181
|
+
The path to check.
|
|
182
|
+
"""
|
|
179
183
|
...
|
|
180
184
|
|
|
181
185
|
|
|
182
186
|
@export
|
|
183
187
|
class AsyncFlatFsReaderWriter(AsyncFlatFsReader, SupportsAsyncWriteChunks, Protocol):
|
|
184
|
-
"""Async
|
|
188
|
+
"""Async variant of the :class:`FlatFsReaderWriter` protocol."""
|
|
185
189
|
|
|
186
190
|
async def remove(self, path: str):
|
|
187
|
-
"""Remove file
|
|
191
|
+
"""Remove existing file.
|
|
188
192
|
|
|
189
|
-
|
|
190
|
-
remove at given *path*.
|
|
193
|
+
This is the async version of :meth:`FlatFsReaderWriter.remove` method.
|
|
191
194
|
|
|
192
195
|
:param path:
|
|
193
196
|
The path to a file to remove.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flatfs"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.4.0"
|
|
4
4
|
description = "Minimal filesystem abstraction with pluggable backends, treating paths as keys and file contents as values."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Maciej Wiatrzyk", email = "maciej.wiatrzyk@gmail.com"}
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Operating System :: OS Independent",
|
|
16
16
|
"Topic :: Software Development :: Libraries",
|
|
17
17
|
]
|
|
18
|
-
requires-python = ">=3.
|
|
18
|
+
requires-python = ">=3.9,<4"
|
|
19
19
|
dependencies = [
|
|
20
20
|
]
|
|
21
21
|
|
|
@@ -28,11 +28,11 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
|
28
28
|
build-backend = "poetry.core.masonry.api"
|
|
29
29
|
|
|
30
30
|
[tool.poetry.group.dev.dependencies]
|
|
31
|
-
pytest = "^
|
|
31
|
+
pytest = "^8.4.2"
|
|
32
32
|
invoke = "^3.0.3"
|
|
33
|
-
pytest-asyncio = "^1.
|
|
33
|
+
pytest-asyncio = "^1.2.0"
|
|
34
34
|
mockify = "^0.14.0"
|
|
35
35
|
pytest-cov = "^7.1.0"
|
|
36
36
|
ruff = "^0.15.12"
|
|
37
|
-
mypy = "^
|
|
37
|
+
mypy = "^1.19.1"
|
|
38
38
|
bumpify = "^0.5.1"
|
flatfs-0.2.0/flatfs/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.2.0"
|
flatfs-0.2.0/flatfs/helpers.py
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
from . import _export
|
|
2
|
-
from .interface import SupportsAsyncReadChunks, SupportsAsyncWriteChunks, SupportsReadChunks, SupportsWriteChunks
|
|
3
|
-
|
|
4
|
-
__all__ = export = _export.Export() # type: ignore
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
@export
|
|
8
|
-
def read_bytes(fs: SupportsReadChunks, path: str, chunk_size: int = 65535) -> bytes:
|
|
9
|
-
"""Read whole file and return as bytes.
|
|
10
|
-
|
|
11
|
-
:param fs:
|
|
12
|
-
FlatFS object to read from.
|
|
13
|
-
|
|
14
|
-
:param path:
|
|
15
|
-
Path to a file to read.
|
|
16
|
-
|
|
17
|
-
:param chunk_size:
|
|
18
|
-
The chunk size to use during read.
|
|
19
|
-
|
|
20
|
-
This is the maximum number of bytes to read from a file per single read
|
|
21
|
-
operation.
|
|
22
|
-
|
|
23
|
-
Defaults to 64kB.
|
|
24
|
-
"""
|
|
25
|
-
out = b""
|
|
26
|
-
for chunk in fs.read_chunks(path, chunk_size):
|
|
27
|
-
out += chunk
|
|
28
|
-
return out
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
@export
|
|
32
|
-
def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
33
|
-
"""Write given data to file at given path either creating the file, or
|
|
34
|
-
replacing with new content.
|
|
35
|
-
|
|
36
|
-
Returns the number of bytes written.
|
|
37
|
-
|
|
38
|
-
:param fs:
|
|
39
|
-
FlatFS object to write to.
|
|
40
|
-
|
|
41
|
-
:param path:
|
|
42
|
-
The path to a file to create or replace.
|
|
43
|
-
|
|
44
|
-
:param data:
|
|
45
|
-
The data to write.
|
|
46
|
-
|
|
47
|
-
:param chunk_size:
|
|
48
|
-
The chunk size to use during writing.
|
|
49
|
-
|
|
50
|
-
If *data* length is greater, it will be split into chunks and written
|
|
51
|
-
chunk by chunk, with each chunk having at most *chunk_size* bytes.
|
|
52
|
-
|
|
53
|
-
Defaults to 64kB.
|
|
54
|
-
"""
|
|
55
|
-
|
|
56
|
-
def gen():
|
|
57
|
-
data_left = data
|
|
58
|
-
while True:
|
|
59
|
-
chunk = data_left[:chunk_size]
|
|
60
|
-
if not chunk:
|
|
61
|
-
break
|
|
62
|
-
data_left = data_left[chunk_size:]
|
|
63
|
-
yield chunk
|
|
64
|
-
|
|
65
|
-
return fs.write_chunks(path, gen())
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
@export
|
|
69
|
-
def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535) -> str:
|
|
70
|
-
"""Read whole text file and return as string.
|
|
71
|
-
|
|
72
|
-
Uses :func:`read_bytes` underneath and then just decodes its output using
|
|
73
|
-
provided *encoding*, which is ``utf-8`` by default.
|
|
74
|
-
"""
|
|
75
|
-
return read_bytes(fs, path, chunk_size).decode(encoding)
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
@export
|
|
79
|
-
def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535):
|
|
80
|
-
"""Create new text file or replace existing with given text data.
|
|
81
|
-
|
|
82
|
-
Uses :func:`write_bytes` underneath and just encodes given *data* with
|
|
83
|
-
provided *encoding*, which is ``utf-8`` by default.
|
|
84
|
-
"""
|
|
85
|
-
return write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
@export
|
|
89
|
-
async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: int = 65535) -> bytes:
|
|
90
|
-
"""Same as :func:`read_bytes`, but for async code."""
|
|
91
|
-
out = b""
|
|
92
|
-
async for chunk in fs.read_chunks(path, chunk_size):
|
|
93
|
-
out += chunk
|
|
94
|
-
return out
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@export
|
|
98
|
-
async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
99
|
-
"""Same as :func:`write_bytes`, but for async code."""
|
|
100
|
-
|
|
101
|
-
async def gen():
|
|
102
|
-
data_left = data
|
|
103
|
-
while True:
|
|
104
|
-
chunk = data_left[:chunk_size]
|
|
105
|
-
if not chunk:
|
|
106
|
-
break
|
|
107
|
-
data_left = data_left[chunk_size:]
|
|
108
|
-
yield chunk
|
|
109
|
-
|
|
110
|
-
return await fs.write_chunks(path, gen())
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
@export
|
|
114
|
-
async def async_read_text(
|
|
115
|
-
fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
116
|
-
) -> str:
|
|
117
|
-
"""Same as :func:`read_text`, but for async code."""
|
|
118
|
-
return (await async_read_bytes(fs, path, chunk_size)).decode(encoding)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
@export
|
|
122
|
-
async def async_write_text(
|
|
123
|
-
fs: SupportsAsyncWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
124
|
-
):
|
|
125
|
-
"""Same as :func:`write_text`, but for async code."""
|
|
126
|
-
await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
@export
|
|
130
|
-
class BinaryReader:
|
|
131
|
-
"""Helper for reading bytes from a file."""
|
|
132
|
-
|
|
133
|
-
def __init__(self, fs: SupportsReadChunks, path: str, chunk_size: int = 65535):
|
|
134
|
-
self.__chunk_gen = fs.read_chunks(path, chunk_size)
|
|
135
|
-
self.__current_chunk = b""
|
|
136
|
-
|
|
137
|
-
def read(self, count: int) -> bytes:
|
|
138
|
-
"""Read at most *count* bytes from an open file.
|
|
139
|
-
|
|
140
|
-
:param count:
|
|
141
|
-
The maximum number of bytes to read.
|
|
142
|
-
"""
|
|
143
|
-
if not self.__current_chunk:
|
|
144
|
-
self.__current_chunk = next(self.__chunk_gen, b"")
|
|
145
|
-
fragment = self.__current_chunk[:count]
|
|
146
|
-
self.__current_chunk = self.__current_chunk[count:]
|
|
147
|
-
return fragment
|
|
148
|
-
|
|
149
|
-
def close(self):
|
|
150
|
-
"""Close this reader.
|
|
151
|
-
|
|
152
|
-
This also closes the underlying file that is opened by flat filesystem
|
|
153
|
-
object given in the constructor.
|
|
154
|
-
|
|
155
|
-
This method is idempotent.
|
|
156
|
-
"""
|
|
157
|
-
self.__current_chunk = b""
|
|
158
|
-
self.__chunk_gen.close()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|