flatfs 0.3.0__tar.gz → 0.5.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,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flatfs
3
- Version: 0.3.0
3
+ Version: 0.5.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,<4
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.5.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
File without changes
@@ -0,0 +1,15 @@
1
+ """The all-in-one import helper for :mod:`flatfs.aio` module."""
2
+
3
+ from .backends import __all__ as _backends_all
4
+ from .helpers import __all__ as _helpers_all
5
+ from .interface import __all__ as _interface_all
6
+
7
+ from .backends import *
8
+ from .helpers import *
9
+ from .interface import *
10
+
11
+ __all__ = (
12
+ *_backends_all,
13
+ *_helpers_all,
14
+ *_interface_all,
15
+ ) # type: ignore
@@ -0,0 +1,90 @@
1
+ import asyncio
2
+ from queue import Queue
3
+ from typing import AsyncGenerator, AsyncIterator, Union
4
+
5
+ from flatfs import _export, _utils
6
+ from flatfs.interface import FlatFsReaderWriter, Stat
7
+
8
+ __all__ = export = _export.Export() # type: ignore
9
+
10
+
11
+ @export
12
+ class AsyncFlatFsAdapter:
13
+ """Adapter that wraps given :class:`flatfs.interface.FlatFsReaderWriter`
14
+ with async interface.
15
+
16
+ You can use this to adapt e.g. :class:`LocalFlatFs` or
17
+ :class:`InMemoryFlatFs` objects with async interface allowing for
18
+ convenient use with async code. The blocking calls are dispatched to
19
+ background worker which communicates with the caller using queues.
20
+
21
+ :param target:
22
+ The target FlatFS reader-writer to wrap.
23
+ """
24
+
25
+ def __init__(self, target: FlatFsReaderWriter):
26
+ self.__target = target
27
+
28
+ async def scan(self) -> AsyncGenerator[str, None]:
29
+
30
+ def scanner():
31
+ for path in self.__target.scan():
32
+ loop.call_soon_threadsafe(queue.put_nowait, path)
33
+ loop.call_soon_threadsafe(queue.put_nowait, "")
34
+
35
+ loop = asyncio.get_running_loop()
36
+ asyncio.create_task(_utils.run_blocking(scanner))
37
+ queue: asyncio.Queue[str] = asyncio.Queue()
38
+ while True:
39
+ path = await queue.get()
40
+ if not path:
41
+ break
42
+ yield path
43
+
44
+ async def exists(self, path: str) -> bool:
45
+ return await _utils.run_blocking(self.__target.exists, path)
46
+
47
+ async def stat(self, path: str) -> Stat:
48
+ return await _utils.run_blocking(self.__target.stat, path)
49
+
50
+ async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncIterator[bytes]:
51
+
52
+ def reader():
53
+ try:
54
+ for chunk in self.__target.read_chunks(path, chunk_size):
55
+ loop.call_soon_threadsafe(queue.put_nowait, chunk)
56
+ loop.call_soon_threadsafe(queue.put_nowait, b"")
57
+ except Exception as e:
58
+ loop.call_soon_threadsafe(queue.put_nowait, e)
59
+
60
+ loop = asyncio.get_running_loop()
61
+ queue: asyncio.Queue[Union[bytes, Exception]] = asyncio.Queue()
62
+ asyncio.create_task(_utils.run_blocking(reader))
63
+ while True:
64
+ chunk_or_exc = await queue.get()
65
+ if not chunk_or_exc:
66
+ break
67
+ if isinstance(chunk_or_exc, Exception):
68
+ raise chunk_or_exc
69
+ yield chunk_or_exc
70
+
71
+ async def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
72
+
73
+ def blocking_writer():
74
+ for write_count in self.__target.write_chunks(path, _utils.generate_chunks_from_queue(chunk_queue)):
75
+ loop.call_soon_threadsafe(ack_queue.put_nowait, write_count)
76
+
77
+ loop = asyncio.get_running_loop()
78
+ chunk_queue: Queue[bytes] = Queue()
79
+ ack_queue: asyncio.Queue[int] = asyncio.Queue()
80
+ task = asyncio.create_task(_utils.run_blocking(blocking_writer))
81
+ try:
82
+ async for chunk in chunk_gen:
83
+ chunk_queue.put(chunk)
84
+ yield await ack_queue.get()
85
+ finally:
86
+ chunk_queue.put(b"")
87
+ await task
88
+
89
+ async def remove(self, path: str):
90
+ await _utils.run_blocking(self.__target.remove, path)
@@ -0,0 +1,148 @@
1
+ import asyncio
2
+ from contextlib import AbstractAsyncContextManager
3
+ from typing import AsyncIterator, Optional
4
+
5
+ from flatfs import _export, _utils, _compat
6
+
7
+ from .interface import SupportsAsyncReadChunks, SupportsAsyncWriteChunks
8
+
9
+ __all__ = export = _export.Export() # type: ignore
10
+
11
+
12
+ @export
13
+ async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: int = 65535) -> bytes:
14
+ """Async variant of the :func:`read_bytes` helper."""
15
+ out = b""
16
+ async for chunk in fs.read_chunks(path, chunk_size):
17
+ out += chunk
18
+ return out
19
+
20
+
21
+ @export
22
+ async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
23
+ """Async variant of the :func:`write_bytes` helper."""
24
+ total_bytes = 0
25
+ async for byte_count in fs.write_chunks(path, _utils.async_split_into_chunks(data, chunk_size)):
26
+ total_bytes += byte_count
27
+ return total_bytes
28
+
29
+
30
+ @export
31
+ async def async_read_text(
32
+ fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535
33
+ ) -> str:
34
+ """Async variant of the :func:`read_text` helper."""
35
+ return (await async_read_bytes(fs, path, chunk_size)).decode(encoding)
36
+
37
+
38
+ @export
39
+ async def async_write_text(
40
+ fs: SupportsAsyncWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535
41
+ ):
42
+ """Async variant of the :func:`write_text` helper."""
43
+ await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
44
+
45
+
46
+ @export
47
+ class AsyncBufferedWriter(AbstractAsyncContextManager):
48
+ """Async variant of the :class:`BufferedWriter` class."""
49
+
50
+ def __init__(self, fs: SupportsAsyncWriteChunks, path: str, buffer_size: int = 65535):
51
+ self.__chunk_queue: asyncio.Queue[bytes] = asyncio.Queue()
52
+ self.__buffer_size = buffer_size
53
+ self.__buffer = b""
54
+ self.__writer = fs.write_chunks(path, _utils.async_generate_chunks_from_queue(self.__chunk_queue))
55
+
56
+ async def __aenter__(self) -> "AsyncBufferedWriter":
57
+ return self
58
+
59
+ async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
60
+ return await self.close()
61
+
62
+ async def __write_chunk(self, chunk: bytes):
63
+ self.__chunk_queue.put_nowait(chunk)
64
+ await _compat.anext(self.__writer)
65
+
66
+ async def write(self, data: bytes) -> int:
67
+ self.__buffer += data
68
+ while len(self.__buffer) > self.__buffer_size:
69
+ chunk = self.__buffer[: self.__buffer_size]
70
+ await self.__write_chunk(chunk)
71
+ self.__buffer = self.__buffer[self.__buffer_size :]
72
+ return len(data)
73
+
74
+ async def flush(self):
75
+ if not self.__buffer:
76
+ return # Nothing to do
77
+ await self.__write_chunk(self.__buffer)
78
+ self.__buffer = b""
79
+
80
+ async def close(self):
81
+ await self.flush()
82
+ await self.__writer.aclose()
83
+
84
+
85
+ @export
86
+ class AsyncBufferedReader(AbstractAsyncContextManager):
87
+ """Async variant of the :class:`BufferedReader` class."""
88
+
89
+ def __init__(self, fs: SupportsAsyncReadChunks, path: str, buffer_size: int = 65535):
90
+ self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
91
+ self.__buffer = b""
92
+
93
+ async def __aenter__(self) -> "AsyncBufferedReader":
94
+ return self
95
+
96
+ async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
97
+ return await self.close()
98
+
99
+ async def close(self):
100
+ self.__buffer = b""
101
+ await self.__reader.aclose()
102
+
103
+ async def read(self, count: int) -> bytes:
104
+ if not self.__buffer:
105
+ self.__buffer = await _compat.anext(self.__reader, b"")
106
+ data = self.__buffer[:count]
107
+ self.__buffer = self.__buffer[count:]
108
+ return data
109
+
110
+
111
+ @export
112
+ class AsyncBufferedTextReader(AbstractAsyncContextManager):
113
+ """Async variant of the :class:`BufferedTextReader` class."""
114
+
115
+ def __init__(self, fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", buffer_size: int = 65535):
116
+ self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
117
+ self.__encoding = encoding
118
+ self.__buffer = ""
119
+
120
+ async def __read_next_chunk(self) -> str:
121
+ return (await _compat.anext(self.__reader, b"")).decode(self.__encoding)
122
+
123
+ async def __aenter__(self) -> "AsyncBufferedTextReader":
124
+ return self
125
+
126
+ async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
127
+ return await self.close()
128
+
129
+ async def __aiter__(self) -> AsyncIterator[str]:
130
+ while next_chunk := await self.__read_next_chunk():
131
+ self.__buffer += next_chunk
132
+ lines = self.__buffer.splitlines()
133
+ if len(lines) > 1:
134
+ for line in lines[:-1]:
135
+ yield line
136
+ self.__buffer = lines[-1]
137
+ yield self.__buffer
138
+
139
+ async def read(self, count: int) -> str:
140
+ if not self.__buffer:
141
+ self.__buffer = await self.__read_next_chunk()
142
+ out = self.__buffer[:count]
143
+ self.__buffer = self.__buffer[count:]
144
+ return out
145
+
146
+ async def close(self):
147
+ self.__buffer = ""
148
+ await self.__reader.aclose()
@@ -0,0 +1,91 @@
1
+ from typing import AsyncGenerator, AsyncIterator, Protocol
2
+
3
+ from flatfs import _export
4
+ from flatfs.interface import Stat
5
+
6
+ __all__ = export = _export.Export() # type: ignore
7
+
8
+
9
+ @export
10
+ class SupportsAsyncReadChunks(Protocol):
11
+ """Async variant of the :class:`SupportsReadChunks` protocol."""
12
+
13
+ def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
14
+ """Return a generator for reading file in chunks of given maximum size.
15
+
16
+ This is the async version of :meth:`SupportsReadChunks.read_chunks`
17
+ method.
18
+
19
+ :param path:
20
+ The path to a file to read.
21
+
22
+ :param chunk_size:
23
+ The maximum chunk size, in bytes.
24
+ """
25
+ ...
26
+
27
+
28
+ @export
29
+ class SupportsAsyncWriteChunks(Protocol):
30
+ """Async variant of the :class:`SupportsWriteChunks` protocol."""
31
+
32
+ def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
33
+ """Generator for creating or overwriting files.
34
+
35
+ This is the async version of :meth:`SupportsWriteChunks.write_chunks`
36
+ method.
37
+
38
+ :param path:
39
+ The path to a file to create or replace.
40
+
41
+ :param chunk_gen:
42
+ Async generator yielding file chunks.
43
+ """
44
+ ...
45
+
46
+
47
+ @export
48
+ class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
49
+ """Async variant of the :class:`FlatFsReader` protocol."""
50
+
51
+ def scan(self) -> AsyncIterator[str]:
52
+ """Return iterator that yields normalized paths to existing files.
53
+
54
+ This is the async version of :meth:`FlatFsReader.scan` method.
55
+ """
56
+ ...
57
+
58
+ async def stat(self, path: str) -> Stat:
59
+ """Get stats of a file at given path.
60
+
61
+ This is the async version of :meth:`FlatFsReader.stat` method.
62
+
63
+ :param path:
64
+ The path to a file.
65
+ """
66
+ ...
67
+
68
+ async def exists(self, path: str) -> bool:
69
+ """Check if file at given path exists.
70
+
71
+ This is the async version of :meth:`FlatFsReader.exists` method.
72
+
73
+ :param path:
74
+ The path to check.
75
+ """
76
+ ...
77
+
78
+
79
+ @export
80
+ class AsyncFlatFsReaderWriter(AsyncFlatFsReader, SupportsAsyncWriteChunks, Protocol):
81
+ """Async variant of the :class:`FlatFsReaderWriter` protocol."""
82
+
83
+ async def remove(self, path: str):
84
+ """Remove existing file.
85
+
86
+ This is the async version of :meth:`FlatFsReaderWriter.remove` method.
87
+
88
+ :param path:
89
+ The path to a file to remove.
90
+ """
91
+ ...
@@ -1,13 +1,21 @@
1
1
  """The all-in-one import helper for FlatFS public API."""
2
2
 
3
+ from .aio.api import __all__ as _aio_all
3
4
  from .backends import __all__ as _backends_all
4
5
  from .exc import __all__ as _exc_all
5
6
  from .helpers import __all__ as _helpers_all
6
7
  from .interface import __all__ as _interface_all
7
8
 
9
+ from .aio.api import *
8
10
  from .backends import *
9
11
  from .exc import *
10
12
  from .helpers import *
11
13
  from .interface import *
12
14
 
13
- __all__ = _backends_all + _exc_all + _helpers_all + _interface_all # type: ignore
15
+ __all__ = (
16
+ *_aio_all,
17
+ *_backends_all,
18
+ *_exc_all,
19
+ *_helpers_all,
20
+ *_interface_all,
21
+ ) # type: ignore
@@ -1,14 +1,12 @@
1
- import asyncio
2
1
  import dataclasses
3
2
  import datetime
4
3
  import fnmatch
5
4
  import os
6
5
  import pathlib
7
- from queue import Queue
8
- from typing import AsyncGenerator, AsyncIterator, Generator, Iterator, Optional
6
+ from typing import Generator, Iterator, Optional
9
7
 
10
8
  from flatfs.exc import PathAccessError, PathNotFoundError
11
- from flatfs.interface import FlatFsReaderWriter, Stat
9
+ from flatfs.interface import Stat
12
10
 
13
11
  from . import _utils, _export
14
12
 
@@ -181,85 +179,3 @@ class InMemoryFlatFs:
181
179
  if key not in self.__storage:
182
180
  raise PathNotFoundError(path)
183
181
  del self.__storage[key]
184
-
185
-
186
- @export
187
- class AsyncFlatFsAdapter:
188
- """Adapter that wraps given :class:`flatfs.interface.FlatFsReaderWriter`
189
- with async interface.
190
-
191
- You can use this to adapt e.g. :class:`LocalFlatFs` or
192
- :class:`InMemoryFlatFs` objects with async interface allowing for
193
- convenient use with async code. The blocking calls are dispatched to
194
- background worker which communicates with the caller using queues.
195
-
196
- :param target:
197
- The target FlatFS reader-writer to wrap.
198
- """
199
-
200
- def __init__(self, target: FlatFsReaderWriter):
201
- self.__target = target
202
-
203
- async def scan(self) -> AsyncGenerator[str, None]:
204
-
205
- def scanner():
206
- for path in self.__target.scan():
207
- loop.call_soon_threadsafe(queue.put_nowait, path)
208
- loop.call_soon_threadsafe(queue.put_nowait, "")
209
-
210
- loop = asyncio.get_running_loop()
211
- asyncio.create_task(_utils.run_blocking(scanner))
212
- queue: asyncio.Queue[str] = asyncio.Queue()
213
- while True:
214
- path = await queue.get()
215
- if not path:
216
- break
217
- yield path
218
-
219
- async def exists(self, path: str) -> bool:
220
- return await _utils.run_blocking(self.__target.exists, path)
221
-
222
- async def stat(self, path: str) -> Stat:
223
- return await _utils.run_blocking(self.__target.stat, path)
224
-
225
- async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncIterator[bytes]:
226
-
227
- def reader():
228
- try:
229
- for chunk in self.__target.read_chunks(path, chunk_size):
230
- loop.call_soon_threadsafe(queue.put_nowait, chunk)
231
- loop.call_soon_threadsafe(queue.put_nowait, b"")
232
- except Exception as e:
233
- loop.call_soon_threadsafe(queue.put_nowait, e)
234
-
235
- loop = asyncio.get_running_loop()
236
- queue: asyncio.Queue[bytes | Exception] = asyncio.Queue()
237
- asyncio.create_task(_utils.run_blocking(reader))
238
- while True:
239
- chunk_or_exc = await queue.get()
240
- if not chunk_or_exc:
241
- break
242
- if isinstance(chunk_or_exc, Exception):
243
- raise chunk_or_exc
244
- yield chunk_or_exc
245
-
246
- async def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
247
-
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)
251
-
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
263
-
264
- async def remove(self, path: str):
265
- await _utils.run_blocking(self.__target.remove, path)
@@ -1,11 +1,9 @@
1
- import asyncio
2
- from contextlib import AbstractAsyncContextManager, AbstractContextManager
1
+ from contextlib import AbstractContextManager
3
2
  from queue import Queue
4
- from types import TracebackType
5
- from typing import Iterator, AsyncIterator
3
+ from typing import Iterator, Optional
6
4
 
7
5
  from . import _export, _utils
8
- from .interface import SupportsAsyncReadChunks, SupportsAsyncWriteChunks, SupportsReadChunks, SupportsWriteChunks
6
+ from .interface import SupportsReadChunks, SupportsWriteChunks
9
7
 
10
8
  __all__ = export = _export.Export() # type: ignore
11
9
 
@@ -34,15 +32,6 @@ def read_bytes(fs: SupportsReadChunks, path: str, chunk_size: int = 65535) -> by
34
32
  return out
35
33
 
36
34
 
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
35
  @export
47
36
  def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
48
37
  """Write given data to file at given path either creating the file, or
@@ -73,15 +62,6 @@ def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int
73
62
  return total_bytes
74
63
 
75
64
 
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
65
  @export
86
66
  def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535) -> str:
87
67
  """Read whole text file and return as string.
@@ -106,14 +86,6 @@ def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_
106
86
  return read_bytes(fs, path, chunk_size).decode(encoding)
107
87
 
108
88
 
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
89
  @export
118
90
  def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535):
119
91
  """Create new text file or replace existing with given text data.
@@ -138,14 +110,6 @@ def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "u
138
110
  return write_bytes(fs, path, data.encode(encoding), chunk_size)
139
111
 
140
112
 
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
113
  @export
150
114
  class BufferedWriter(AbstractContextManager):
151
115
  """Buffered writer for creating new or replacing existing files.
@@ -180,9 +144,7 @@ class BufferedWriter(AbstractContextManager):
180
144
  def __enter__(self) -> "BufferedWriter":
181
145
  return self
182
146
 
183
- def __exit__(
184
- self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
185
- ) -> bool | None:
147
+ def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
186
148
  return self.close()
187
149
 
188
150
  def __write_chunk(self, chunk: bytes):
@@ -221,47 +183,6 @@ class BufferedWriter(AbstractContextManager):
221
183
  return len(data)
222
184
 
223
185
 
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
186
  @export
266
187
  class BufferedReader(AbstractContextManager):
267
188
  """Helper for buffered reading from underlying flat filesystem.
@@ -292,9 +213,7 @@ class BufferedReader(AbstractContextManager):
292
213
  def __enter__(self) -> "BufferedReader":
293
214
  return self
294
215
 
295
- def __exit__(
296
- self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
297
- ) -> bool | None:
216
+ def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
298
217
  return self.close()
299
218
 
300
219
  def close(self):
@@ -309,34 +228,6 @@ class BufferedReader(AbstractContextManager):
309
228
  return data
310
229
 
311
230
 
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
231
  @export
341
232
  class BufferedTextReader(AbstractContextManager):
342
233
  """Equivalent of :class:`BufferedReader` for text files.
@@ -371,9 +262,7 @@ class BufferedTextReader(AbstractContextManager):
371
262
  def __enter__(self) -> "BufferedTextReader":
372
263
  return self
373
264
 
374
- def __exit__(
375
- self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
376
- ) -> bool | None:
265
+ def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
377
266
  return self.close()
378
267
 
379
268
  def __iter__(self) -> Iterator[str]:
@@ -405,45 +294,3 @@ class BufferedTextReader(AbstractContextManager):
405
294
  """
406
295
  self.__buffer = ""
407
296
  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, AsyncIterator, Generator, Iterator, Protocol
3
+ from typing import Generator, Iterator, Protocol
4
4
 
5
5
  from . import _export
6
6
 
@@ -111,88 +111,3 @@ class FlatFsReaderWriter(FlatFsReader, SupportsWriteChunks, Protocol):
111
111
  The path to a file to remove.
112
112
  """
113
113
  ...
114
-
115
-
116
- @export
117
- class SupportsAsyncReadChunks(Protocol):
118
- """Async variant of the :class:`SupportsReadChunks` protocol."""
119
-
120
- def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
121
- """Return a generator for reading file in chunks of given maximum size.
122
-
123
- This is the async version of :meth:`SupportsReadChunks.read_chunks`
124
- method.
125
-
126
- :param path:
127
- The path to a file to read.
128
-
129
- :param chunk_size:
130
- The maximum chunk size, in bytes.
131
- """
132
- ...
133
-
134
-
135
- @export
136
- class SupportsAsyncWriteChunks(Protocol):
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
-
142
- This is the async version of :meth:`SupportsWriteChunks.write_chunks`
143
- method.
144
-
145
- :param path:
146
- The path to a file to create or replace.
147
-
148
- :param chunk_gen:
149
- Async generator yielding file chunks.
150
- """
151
- ...
152
-
153
-
154
- @export
155
- class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
156
- """Async variant of the :class:`FlatFsReader` protocol."""
157
-
158
- def scan(self) -> AsyncIterator[str]:
159
- """Return iterator that yields normalized paths to existing files.
160
-
161
- This is the async version of :meth:`FlatFsReader.scan` method.
162
- """
163
- ...
164
-
165
- async def stat(self, path: str) -> Stat:
166
- """Get stats of a file at given path.
167
-
168
- This is the async version of :meth:`FlatFsReader.stat` method.
169
-
170
- :param path:
171
- The path to a file.
172
- """
173
- ...
174
-
175
- async def exists(self, path: str) -> bool:
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
- """
183
- ...
184
-
185
-
186
- @export
187
- class AsyncFlatFsReaderWriter(AsyncFlatFsReader, SupportsAsyncWriteChunks, Protocol):
188
- """Async variant of the :class:`FlatFsReaderWriter` protocol."""
189
-
190
- async def remove(self, path: str):
191
- """Remove existing file.
192
-
193
- This is the async version of :meth:`FlatFsReaderWriter.remove` method.
194
-
195
- :param path:
196
- The path to a file to remove.
197
- """
198
- ...
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "flatfs"
3
- version = "0.3.0"
3
+ version = "0.5.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.10,<4"
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 = "^9.0.3"
31
+ pytest = "^8.4.2"
32
32
  invoke = "^3.0.3"
33
- pytest-asyncio = "^1.3.0"
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 = "^2.0.0"
37
+ mypy = "^1.19.1"
38
38
  bumpify = "^0.5.1"
@@ -1 +0,0 @@
1
- __version__ = "0.3.0"
File without changes
File without changes
File without changes
File without changes
File without changes