flatfs 0.2.0__tar.gz → 0.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatfs
3
- Version: 0.2.0
3
+ Version: 0.3.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
@@ -0,0 +1 @@
1
+ __version__ = "0.3.0"
@@ -1,7 +1,8 @@
1
1
  import asyncio
2
2
  import datetime
3
3
  import functools
4
- from typing import Callable, TypeVar
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, AsyncIterable, Generator, Iterable, Iterator, Optional
8
+ from typing import AsyncGenerator, AsyncIterator, Generator, Iterator, Optional
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, chunks: Iterable[bytes]) -> int:
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 chunks:
116
- total_bytes += fd.write(chunk)
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, chunks: Iterable[bytes]) -> int:
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=len(payload),
170
+ size=0,
174
171
  )
175
- self.__storage[self.__make_key(path)] = self._File(payload, stat)
176
- return stat.size
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) -> AsyncGenerator[bytes, None]:
225
+ async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncIterator[bytes]:
225
226
 
226
227
  def reader():
227
228
  try:
@@ -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, chunks: AsyncIterable[bytes]) -> int:
246
+ async def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
246
247
 
247
- def gen() -> Generator[bytes, None, None]:
248
- while True:
249
- chunk = queue.get()
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
- queue: Queue[bytes] = Queue()
255
- task = asyncio.create_task(_utils.run_blocking(self.__target.write_chunks, path, gen()))
256
- async for chunk in chunks:
257
- queue.put(chunk)
258
- queue.put(b"")
259
- return await task
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,449 @@
1
+ import asyncio
2
+ from contextlib import AbstractAsyncContextManager, AbstractContextManager
3
+ from queue import Queue
4
+ from types import TracebackType
5
+ from typing import Iterator, AsyncIterator
6
+
7
+ from . import _export, _utils
8
+ from .interface import SupportsAsyncReadChunks, SupportsAsyncWriteChunks, SupportsReadChunks, SupportsWriteChunks
9
+
10
+ __all__ = export = _export.Export() # type: ignore
11
+
12
+
13
+ @export
14
+ def read_bytes(fs: SupportsReadChunks, path: str, chunk_size: int = 65535) -> bytes:
15
+ """Read whole file and return as bytes.
16
+
17
+ :param fs:
18
+ FlatFS object to read from.
19
+
20
+ :param path:
21
+ Path to a file to read.
22
+
23
+ :param chunk_size:
24
+ The chunk size to use during read.
25
+
26
+ This is the maximum number of bytes to read from a file per single read
27
+ operation.
28
+
29
+ Defaults to 64kB.
30
+ """
31
+ out = b""
32
+ for chunk in fs.read_chunks(path, chunk_size):
33
+ out += chunk
34
+ return out
35
+
36
+
37
+ @export
38
+ async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: int = 65535) -> bytes:
39
+ """Async variant of the :func:`read_bytes` helper."""
40
+ out = b""
41
+ async for chunk in fs.read_chunks(path, chunk_size):
42
+ out += chunk
43
+ return out
44
+
45
+
46
+ @export
47
+ def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
48
+ """Write given data to file at given path either creating the file, or
49
+ replacing with new content.
50
+
51
+ Returns the number of bytes written.
52
+
53
+ :param fs:
54
+ FlatFS object to write to.
55
+
56
+ :param path:
57
+ The path to a file to create or replace.
58
+
59
+ :param data:
60
+ The data to write.
61
+
62
+ :param chunk_size:
63
+ The chunk size to use during writing.
64
+
65
+ If *data* length is greater, it will be split into chunks and written
66
+ chunk by chunk, with each chunk having at most *chunk_size* bytes.
67
+
68
+ Defaults to 64kB.
69
+ """
70
+ total_bytes = 0
71
+ for byte_count in fs.write_chunks(path, _utils.split_into_chunks(data, chunk_size)):
72
+ total_bytes += byte_count
73
+ return total_bytes
74
+
75
+
76
+ @export
77
+ async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
78
+ """Async variant of the :func:`write_bytes` helper."""
79
+ total_bytes = 0
80
+ async for byte_count in fs.write_chunks(path, _utils.async_split_into_chunks(data, chunk_size)):
81
+ total_bytes += byte_count
82
+ return total_bytes
83
+
84
+
85
+ @export
86
+ def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535) -> str:
87
+ """Read whole text file and return as string.
88
+
89
+ Uses :func:`read_bytes` underneath and then just decodes its output using
90
+ provided *encoding*, which is ``utf-8`` by default.
91
+
92
+ :param fs:
93
+ FlatFS object to read from.
94
+
95
+ :param path:
96
+ The path to a file to read.
97
+
98
+ :param encoding:
99
+ The encoding to use.
100
+
101
+ :param chunk_size:
102
+ The chunk size to use during reading.
103
+
104
+ This tells how many bytes can at most be read in single pass.
105
+ """
106
+ return read_bytes(fs, path, chunk_size).decode(encoding)
107
+
108
+
109
+ @export
110
+ async def async_read_text(
111
+ fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535
112
+ ) -> str:
113
+ """Async variant of the :func:`read_text` helper."""
114
+ return (await async_read_bytes(fs, path, chunk_size)).decode(encoding)
115
+
116
+
117
+ @export
118
+ def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535):
119
+ """Create new text file or replace existing with given text data.
120
+
121
+ Uses :func:`write_bytes` underneath and just encodes given *data* with
122
+ provided *encoding*, which is ``utf-8`` by default.
123
+
124
+ :param fs:
125
+ FlatFS object to write to.
126
+
127
+ :param path:
128
+ The path to a file to create or replace.
129
+
130
+ :param encoding:
131
+ The encoding to use.
132
+
133
+ :param chunk_size:
134
+ The chunk size to use during writing.
135
+
136
+ This tells how many bytes can at most be written in a single pass.
137
+ """
138
+ return write_bytes(fs, path, data.encode(encoding), chunk_size)
139
+
140
+
141
+ @export
142
+ async def async_write_text(
143
+ fs: SupportsAsyncWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535
144
+ ):
145
+ """Async variant of the :func:`write_text` helper."""
146
+ await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
147
+
148
+
149
+ @export
150
+ class BufferedWriter(AbstractContextManager):
151
+ """Buffered writer for creating new or replacing existing files.
152
+
153
+ It allows efficient writing of arbitrary binary data thanks to the buffer
154
+ being used. Once the length of buffered data exceeds given buffer size, the
155
+ data is flushed to the underlying filesystem and buffer is cleared. This
156
+ allows to limit actual write operations to minimum.
157
+
158
+ This helper can be used as a context manager.
159
+
160
+ :param fs:
161
+ FlatFS object to write to.
162
+
163
+ :param path:
164
+ Path to a file to create or replace.
165
+
166
+ :param buffer_size:
167
+ The size of a buffer, in bytes.
168
+
169
+ This is the maximum amount of data that can be buffered before it is
170
+ flushed to the backend. This is equivalent to the maximum size of a
171
+ single chunk that is written to the underlying filesystem.
172
+ """
173
+
174
+ def __init__(self, fs: SupportsWriteChunks, path: str, buffer_size: int = 65535):
175
+ self.__chunk_queue: Queue[bytes] = Queue()
176
+ self.__buffer_size = buffer_size
177
+ self.__buffer = b""
178
+ self.__writer = fs.write_chunks(path, _utils.generate_chunks_from_queue(self.__chunk_queue))
179
+
180
+ def __enter__(self) -> "BufferedWriter":
181
+ return self
182
+
183
+ def __exit__(
184
+ self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
185
+ ) -> bool | None:
186
+ return self.close()
187
+
188
+ def __write_chunk(self, chunk: bytes):
189
+ self.__chunk_queue.put(chunk)
190
+ next(self.__writer)
191
+
192
+ def flush(self):
193
+ """Flush the buffer and clear it.
194
+
195
+ This is automatically called on close or context manager exit.
196
+ """
197
+ if not self.__buffer:
198
+ return # Nothing to do
199
+ self.__write_chunk(self.__buffer)
200
+ self.__buffer = b""
201
+
202
+ def close(self):
203
+ """Flush remaining data and close underlying file."""
204
+ self.flush()
205
+ return self.__writer.close()
206
+
207
+ def write(self, data: bytes) -> int:
208
+ """Write data to the buffer.
209
+
210
+ If the buffer is full, it will be flushed and remaining data (if any)
211
+ will be used to initialize emptied buffer.
212
+
213
+ :param data:
214
+ The data to write.
215
+ """
216
+ self.__buffer += data
217
+ while len(self.__buffer) > self.__buffer_size:
218
+ chunk = self.__buffer[: self.__buffer_size]
219
+ self.__write_chunk(chunk)
220
+ self.__buffer = self.__buffer[self.__buffer_size :]
221
+ return len(data)
222
+
223
+
224
+ @export
225
+ class AsyncBufferedWriter(AbstractAsyncContextManager):
226
+ """Async variant of the :class:`BufferedWriter` class."""
227
+
228
+ def __init__(self, fs: SupportsAsyncWriteChunks, path: str, buffer_size: int = 65535):
229
+ self.__chunk_queue: asyncio.Queue[bytes] = asyncio.Queue()
230
+ self.__buffer_size = buffer_size
231
+ self.__buffer = b""
232
+ self.__writer = fs.write_chunks(path, _utils.async_generate_chunks_from_queue(self.__chunk_queue))
233
+
234
+ async def __aenter__(self) -> "AsyncBufferedWriter":
235
+ return self
236
+
237
+ async def __aexit__(
238
+ self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
239
+ ) -> bool | None:
240
+ return await self.close()
241
+
242
+ async def __write_chunk(self, chunk: bytes):
243
+ self.__chunk_queue.put_nowait(chunk)
244
+ await anext(self.__writer)
245
+
246
+ async def write(self, data: bytes) -> int:
247
+ self.__buffer += data
248
+ while len(self.__buffer) > self.__buffer_size:
249
+ chunk = self.__buffer[: self.__buffer_size]
250
+ await self.__write_chunk(chunk)
251
+ self.__buffer = self.__buffer[self.__buffer_size :]
252
+ return len(data)
253
+
254
+ async def flush(self):
255
+ if not self.__buffer:
256
+ return # Nothing to do
257
+ await self.__write_chunk(self.__buffer)
258
+ self.__buffer = b""
259
+
260
+ async def close(self):
261
+ await self.flush()
262
+ await self.__writer.aclose()
263
+
264
+
265
+ @export
266
+ class BufferedReader(AbstractContextManager):
267
+ """Helper for buffered reading from underlying flat filesystem.
268
+
269
+ If reads from an underlying filesystem chunks of bytes of given maximum
270
+ length and stores it locally for reading. When entire buffer is read, the
271
+ next chunk is fetched from the filesystem. The whole operation is repeated
272
+ until file is fully read or the reader is closed.
273
+
274
+ This helper can be used via context manager.
275
+
276
+ :param fs:
277
+ The filesystem to read from.
278
+
279
+ :param path:
280
+ The path to a file to read.
281
+
282
+ :param buffer_size:
283
+ The size of a read buffer, in bytes.
284
+
285
+ This is also the maximum size of a single chunk read from a file.
286
+ """
287
+
288
+ def __init__(self, fs: SupportsReadChunks, path: str, buffer_size: int = 65535):
289
+ self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
290
+ self.__buffer = b""
291
+
292
+ def __enter__(self) -> "BufferedReader":
293
+ return self
294
+
295
+ def __exit__(
296
+ self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
297
+ ) -> bool | None:
298
+ return self.close()
299
+
300
+ def close(self):
301
+ self.__buffer = b""
302
+ self.__reader.close()
303
+
304
+ def read(self, count: int) -> bytes:
305
+ if not self.__buffer:
306
+ self.__buffer = next(self.__reader, b"")
307
+ data = self.__buffer[:count]
308
+ self.__buffer = self.__buffer[count:]
309
+ return data
310
+
311
+
312
+ @export
313
+ class AsyncBufferedReader(AbstractAsyncContextManager):
314
+ """Async variant of the :class:`BufferedReader` class."""
315
+
316
+ def __init__(self, fs: SupportsAsyncReadChunks, path: str, buffer_size: int = 65535):
317
+ self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
318
+ self.__buffer = b""
319
+
320
+ async def __aenter__(self) -> "AsyncBufferedReader":
321
+ return self
322
+
323
+ async def __aexit__(
324
+ self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
325
+ ) -> bool | None:
326
+ return await self.close()
327
+
328
+ async def close(self):
329
+ self.__buffer = b""
330
+ await self.__reader.aclose()
331
+
332
+ async def read(self, count: int) -> bytes:
333
+ if not self.__buffer:
334
+ self.__buffer = await anext(self.__reader, b"")
335
+ data = self.__buffer[:count]
336
+ self.__buffer = self.__buffer[count:]
337
+ return data
338
+
339
+
340
+ @export
341
+ class BufferedTextReader(AbstractContextManager):
342
+ """Equivalent of :class:`BufferedReader` for text files.
343
+
344
+ It reads at most *buffer_size* bytes from underlying file, stores in local
345
+ buffer, and then streams from that buffer until it expires. Once it is
346
+ expired and more data is needed, the next chunk of data is automatically
347
+ read and stored in the buffer.
348
+
349
+ :param fs:
350
+ The filesystem reader object.
351
+
352
+ :param path:
353
+ The path to a text file to read.
354
+
355
+ :param encoding:
356
+ The encoding to use.
357
+
358
+ :param buffer_size:
359
+ The maximum amount of data to read from an underlying file in one pass,
360
+ in bytes.
361
+ """
362
+
363
+ def __init__(self, fs: SupportsReadChunks, path: str, encoding: str = "utf-8", buffer_size: int = 65535):
364
+ self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
365
+ self.__encoding = encoding
366
+ self.__buffer = ""
367
+
368
+ def __read_next_chunk(self) -> str:
369
+ return next(self.__reader, b"").decode(self.__encoding)
370
+
371
+ def __enter__(self) -> "BufferedTextReader":
372
+ return self
373
+
374
+ def __exit__(
375
+ self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
376
+ ) -> bool | None:
377
+ return self.close()
378
+
379
+ def __iter__(self) -> Iterator[str]:
380
+ while next_chunk := self.__read_next_chunk():
381
+ self.__buffer += next_chunk
382
+ lines = self.__buffer.splitlines()
383
+ if len(lines) > 1:
384
+ for line in lines[:-1]:
385
+ yield line
386
+ self.__buffer = lines[-1]
387
+ yield self.__buffer
388
+
389
+ def read(self, count: int) -> str:
390
+ """Read at most *count* characters.
391
+
392
+ :param count:
393
+ The maximum number of characters to read.
394
+ """
395
+ if not self.__buffer:
396
+ self.__buffer = self.__read_next_chunk()
397
+ out = self.__buffer[:count]
398
+ self.__buffer = self.__buffer[count:]
399
+ return out
400
+
401
+ def close(self):
402
+ """Close this reader.
403
+
404
+ This also closes the underlying file.
405
+ """
406
+ self.__buffer = ""
407
+ self.__reader.close()
408
+
409
+
410
+ @export
411
+ class AsyncBufferedTextReader(AbstractAsyncContextManager):
412
+ """Async variant of the :class:`BufferedTextReader` class."""
413
+
414
+ def __init__(self, fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", buffer_size: int = 65535):
415
+ self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
416
+ self.__encoding = encoding
417
+ self.__buffer = ""
418
+
419
+ async def __read_next_chunk(self) -> str:
420
+ return (await anext(self.__reader, b"")).decode(self.__encoding)
421
+
422
+ async def __aenter__(self) -> "AsyncBufferedTextReader":
423
+ return self
424
+
425
+ async def __aexit__(
426
+ self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
427
+ ) -> bool | None:
428
+ return await self.close()
429
+
430
+ async def __aiter__(self) -> AsyncIterator[str]:
431
+ while next_chunk := await self.__read_next_chunk():
432
+ self.__buffer += next_chunk
433
+ lines = self.__buffer.splitlines()
434
+ if len(lines) > 1:
435
+ for line in lines[:-1]:
436
+ yield line
437
+ self.__buffer = lines[-1]
438
+ yield self.__buffer
439
+
440
+ async def read(self, count: int) -> str:
441
+ if not self.__buffer:
442
+ self.__buffer = await self.__read_next_chunk()
443
+ out = self.__buffer[:count]
444
+ self.__buffer = self.__buffer[count:]
445
+ return out
446
+
447
+ async def close(self):
448
+ self.__buffer = ""
449
+ await self.__reader.aclose()
@@ -1,6 +1,6 @@
1
1
  import dataclasses
2
2
  import datetime
3
- from typing import AsyncGenerator, AsyncIterable, Generator, Iterable, Iterator, Protocol
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
- """Read file in chunks and return iterator yielding chunks.
27
+ """Return generator for reading existing file in chunks of given
28
+ maximum size.
28
29
 
29
- This method can be safely used with large files without the risk of
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, chunks: Iterable[bytes]) -> int:
51
- """Create or overwrite file at given *path* and write chunks of data
52
- generated by provided *chunks* iterable.
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
- Returns the total number of bytes written, which is equal to the size
55
- of a resulting file.
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 to create or modify.
62
+ The path to a file.
59
63
 
60
- :param chunks:
61
- Iterable providing chunks of bytes to write to a file.
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
- """Scan filesystem and generate paths to existing files.
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 *path*.
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 *path* points to an existing file.
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 *path*.
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 *path*.
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
- """Read file in chunks and return iterator yielding chunks.
121
+ """Return a generator for reading file in chunks of given maximum size.
118
122
 
119
- This method can be safely used with large files without the risk of
120
- getting out of memory.
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 (in bytes).
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
- async def write_chunks(self, path: str, chunks: AsyncIterable[bytes]) -> int:
139
- """Create or overwrite file at given *path* and write chunks of data
140
- generated by provided *chunks* iterable.
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
- Returns the total number of bytes written, which is equal to the size
143
- of a resulting file.
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 modify.
146
+ The path to a file to create or replace.
147
147
 
148
- :param chunks:
149
- Iterable providing chunks of bytes to write to a file.
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 read-only interface to access files in the flat filesystem."""
156
+ """Async variant of the :class:`FlatFsReader` protocol."""
157
157
 
158
- def scan(self) -> AsyncGenerator[str, None]:
159
- """Scan filesystem and generate paths to existing files.
158
+ def scan(self) -> AsyncIterator[str]:
159
+ """Return iterator that yields normalized paths to existing files.
160
160
 
161
- Each yielded path can later be used with other methods of this
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 *path*.
166
+ """Get stats of a file at given path.
168
167
 
169
- If the file does not exist, :exc:`flatfs.exc.PathNotFoundError` will be
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 *path* points to an existing file."""
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 read-write interface to access files in the flat filesystem."""
188
+ """Async variant of the :class:`FlatFsReaderWriter` protocol."""
185
189
 
186
190
  async def remove(self, path: str):
187
- """Remove file at given *path*.
191
+ """Remove existing file.
188
192
 
189
- Raises :exc:`flatfs.exc.PathNotFoundError` if there was no file to
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.2.0"
3
+ version = "0.3.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"}
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
@@ -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