flatfs 0.0.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.
@@ -0,0 +1,19 @@
1
+ Copyright 2026 Maciej Wiatrzyk <maciej.wiatrzyk@gmail.com>
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
4
+ this software and associated documentation files (the “Software”), to deal in
5
+ the Software without restriction, including without limitation the rights to
6
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
7
+ of the Software, and to permit persons to whom the Software is furnished to do
8
+ so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in all
11
+ copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19
+ SOFTWARE.
flatfs-0.0.0/PKG-INFO ADDED
@@ -0,0 +1,113 @@
1
+ Metadata-Version: 2.3
2
+ Name: flatfs
3
+ Version: 0.0.0
4
+ Summary: Minimal filesystem abstraction with pluggable backends, treating paths as keys and file contents as values.
5
+ License: MIT
6
+ Keywords: flat,filesystem,abstraction,library
7
+ Author: Maciej Wiatrzyk
8
+ Author-email: maciej.wiatrzyk@gmail.com
9
+ Requires-Python: >=3.10,<4
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Topic :: Software Development :: Libraries
14
+ Project-URL: Bug Tracker, https://github.com/mwiatrzyk/flatfs/issues
15
+ Project-URL: Repository, https://github.com/mwiatrzyk/flatfs
16
+ Description-Content-Type: text/markdown
17
+
18
+ # FlatFS
19
+
20
+ Minimal filesystem abstraction with pluggable backends, treating paths as keys
21
+ and file contents as values.
22
+
23
+ ## About
24
+
25
+ FlatFS provides minimalistic protocol-based API for accessing files. The API
26
+ allows you to:
27
+
28
+ * create files using generator of binary chunks as source of data
29
+ * read files in chunks
30
+ * checking if file exists
31
+ * removing file from the filesystem
32
+ * scanning filesystem to retrieve all the files that are available
33
+
34
+ The API from above is supplied with a rich set of helpers that can simplify
35
+ common actions, like writing/reading whole files in text or binary mode,
36
+ reading files line-by-line, reading exact amount of bytes etc.
37
+
38
+ FlatFS does not use directories; those are just used to organize paths (aka
39
+ keys) and some implementations may use real directories, and the other may not.
40
+
41
+ The library is implemented in a way allowing it to be used with large files
42
+ thanks to the generator-based core API.
43
+
44
+ ## Installation
45
+
46
+ The project can be installed directly from PyPI:
47
+
48
+ ```shell
49
+ $ pip install flatfs
50
+ ```
51
+
52
+ You can alternatively use any of the existing Python package management tools
53
+ (e.g. Poetry or uv) to install the library directly to your project.
54
+
55
+ ## Examples
56
+
57
+ Here are some practical examples of how this library can be used:
58
+
59
+ 1. Access ``/tmp`` directory:
60
+
61
+ ```python
62
+ import pathlib
63
+
64
+ from flatfs.api import LocalFlatFs, FlatFsReaderWriter, write_text, read_text
65
+
66
+ def make_temp_fs() -> FlatFsReaderWriter:
67
+ return LocalFlatFs(pathlib.Path("/tmp"))
68
+
69
+ temp_fs = make_temp_fs()
70
+ write_text(temp_fs, "/foo/bar.txt", "content of file") # Creates text file at /tmp/foo/bar.txt
71
+ content = read_text(temp_fs, "/foo/bar.txt") # Read the content back
72
+ ```
73
+
74
+ 2. Create in-memory flat filesystem (this implementation can be used during testing):
75
+
76
+ ```python
77
+ from flatfs.api import InMemoryFlatFs
78
+
79
+ fs = InMemoryFlatFs()
80
+ fs.write_chunks("foo.txt", [b"spam"])
81
+ paths = list(fs.scan()) # Will return: ["/foo.txt"]
82
+ ```
83
+
84
+ 3. Wrap flat filesystem created earlier with async interface:
85
+
86
+ ```python
87
+ from flatfs.api import AsyncFlatFsAdapter, async_read_text
88
+
89
+ async_temp_fs = AsyncFlatFsAdapter(temp_fs)
90
+ content = await async_read_text(async_temp_fs, "foo/bar.txt") # Paths are normalized and leading / is automatically added
91
+ print(content) # Would print: content of file
92
+ ```
93
+
94
+ ## Rationale
95
+
96
+ During developing of various systems and tools that follow clean architecture
97
+ and layered design (business logic, use case interactors, data ports and
98
+ gateways etc.) I found myself writing similar tool as one of the port/gateway
99
+ pair over and over again. This library was created to have such toolkit a bit
100
+ more advanced, based on chunk generators and producers, backed up with
101
+ comprehensive set of helpers, just to avoid writing it again for the (N+1)th
102
+ time.
103
+
104
+ ## Author
105
+
106
+ Maciej Wiatrzyk <maciej.wiatrzyk@gmail.com>
107
+
108
+ ## License
109
+
110
+ This project is released under the terms of the MIT license.
111
+
112
+ See LICENSE.txt for more details.
113
+
flatfs-0.0.0/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # FlatFS
2
+
3
+ Minimal filesystem abstraction with pluggable backends, treating paths as keys
4
+ and file contents as values.
5
+
6
+ ## About
7
+
8
+ FlatFS provides minimalistic protocol-based API for accessing files. The API
9
+ allows you to:
10
+
11
+ * create files using generator of binary chunks as source of data
12
+ * read files in chunks
13
+ * checking if file exists
14
+ * removing file from the filesystem
15
+ * scanning filesystem to retrieve all the files that are available
16
+
17
+ The API from above is supplied with a rich set of helpers that can simplify
18
+ common actions, like writing/reading whole files in text or binary mode,
19
+ reading files line-by-line, reading exact amount of bytes etc.
20
+
21
+ FlatFS does not use directories; those are just used to organize paths (aka
22
+ keys) and some implementations may use real directories, and the other may not.
23
+
24
+ The library is implemented in a way allowing it to be used with large files
25
+ thanks to the generator-based core API.
26
+
27
+ ## Installation
28
+
29
+ The project can be installed directly from PyPI:
30
+
31
+ ```shell
32
+ $ pip install flatfs
33
+ ```
34
+
35
+ You can alternatively use any of the existing Python package management tools
36
+ (e.g. Poetry or uv) to install the library directly to your project.
37
+
38
+ ## Examples
39
+
40
+ Here are some practical examples of how this library can be used:
41
+
42
+ 1. Access ``/tmp`` directory:
43
+
44
+ ```python
45
+ import pathlib
46
+
47
+ from flatfs.api import LocalFlatFs, FlatFsReaderWriter, write_text, read_text
48
+
49
+ def make_temp_fs() -> FlatFsReaderWriter:
50
+ return LocalFlatFs(pathlib.Path("/tmp"))
51
+
52
+ temp_fs = make_temp_fs()
53
+ write_text(temp_fs, "/foo/bar.txt", "content of file") # Creates text file at /tmp/foo/bar.txt
54
+ content = read_text(temp_fs, "/foo/bar.txt") # Read the content back
55
+ ```
56
+
57
+ 2. Create in-memory flat filesystem (this implementation can be used during testing):
58
+
59
+ ```python
60
+ from flatfs.api import InMemoryFlatFs
61
+
62
+ fs = InMemoryFlatFs()
63
+ fs.write_chunks("foo.txt", [b"spam"])
64
+ paths = list(fs.scan()) # Will return: ["/foo.txt"]
65
+ ```
66
+
67
+ 3. Wrap flat filesystem created earlier with async interface:
68
+
69
+ ```python
70
+ from flatfs.api import AsyncFlatFsAdapter, async_read_text
71
+
72
+ async_temp_fs = AsyncFlatFsAdapter(temp_fs)
73
+ content = await async_read_text(async_temp_fs, "foo/bar.txt") # Paths are normalized and leading / is automatically added
74
+ print(content) # Would print: content of file
75
+ ```
76
+
77
+ ## Rationale
78
+
79
+ During developing of various systems and tools that follow clean architecture
80
+ and layered design (business logic, use case interactors, data ports and
81
+ gateways etc.) I found myself writing similar tool as one of the port/gateway
82
+ pair over and over again. This library was created to have such toolkit a bit
83
+ more advanced, based on chunk generators and producers, backed up with
84
+ comprehensive set of helpers, just to avoid writing it again for the (N+1)th
85
+ time.
86
+
87
+ ## Author
88
+
89
+ Maciej Wiatrzyk <maciej.wiatrzyk@gmail.com>
90
+
91
+ ## License
92
+
93
+ This project is released under the terms of the MIT license.
94
+
95
+ See LICENSE.txt for more details.
File without changes
@@ -0,0 +1,12 @@
1
+ from typing import TypeVar
2
+
3
+ T = TypeVar("T")
4
+
5
+
6
+ class Export(list):
7
+ """Helper for creating ``__all__`` module properties."""
8
+
9
+ def __call__(self, func_or_cls: T) -> T:
10
+ name = getattr(func_or_cls, "__name__")
11
+ self.append(name)
12
+ return func_or_cls
@@ -0,0 +1,52 @@
1
+ import asyncio
2
+ import functools
3
+ from typing import Callable, TypeVar
4
+
5
+ from flatfs.exc import PathAccessError
6
+
7
+ T = TypeVar("T")
8
+
9
+
10
+ def normalize_path(path: str) -> str:
11
+ """Normalizes path by removing duplicated slash characters and by
12
+ evaluating relative path components (e.g. ``../`` or ``./``).
13
+
14
+ Returns normalized path, or raises :exc:`flatfs.exc.FileAccessError` if
15
+ final path would go beyond filesystem bounds.
16
+
17
+ :param path:
18
+ The path to normalize.
19
+ """
20
+ parts: list[str] = []
21
+ for part in path.split("/"):
22
+ if not part or part == ".":
23
+ continue
24
+ if part == "..":
25
+ if not parts:
26
+ raise PathAccessError(path)
27
+ parts.pop()
28
+ else:
29
+ parts.append(part)
30
+ out = "/".join(parts)
31
+ if out[0] == "/":
32
+ return out
33
+ return "/" + out
34
+
35
+
36
+ async def run_blocking(func: Callable[..., T], *args, **kwargs) -> T:
37
+ """Run blocking (i.e. non-async) task in an event loop's default
38
+ executor.
39
+
40
+ On success, return *func*'s result, on failure raise *func*'s exception.
41
+
42
+ :param func:
43
+ The non-async callable to run.
44
+
45
+ :param `*args`:
46
+ Positional args to be passed to *func*.
47
+
48
+ :param `**kwargs`:
49
+ Named args to be passed to *func*.
50
+ """
51
+ wrapped_func = functools.partial(func, *args, **kwargs)
52
+ return await asyncio.get_running_loop().run_in_executor(None, wrapped_func)
@@ -0,0 +1,13 @@
1
+ """The all-in-one import helper for FlatFS public API."""
2
+
3
+ from .backends import __all__ as _backends_all
4
+ from .exc import __all__ as _exc_all
5
+ from .helpers import __all__ as _helpers_all
6
+ from .interface import __all__ as _interface_all
7
+
8
+ from .backends import *
9
+ from .exc import *
10
+ from .helpers import *
11
+ from .interface import *
12
+
13
+ __all__ = _backends_all + _exc_all + _helpers_all + _interface_all # type: ignore
@@ -0,0 +1,227 @@
1
+ import asyncio
2
+ import fnmatch
3
+ import os
4
+ import pathlib
5
+ from queue import Queue
6
+ from typing import AsyncGenerator, AsyncIterable, Generator, Iterable, Iterator, Optional
7
+
8
+ from flatfs.exc import PathAccessError, PathNotFoundError
9
+ from flatfs.interface import FlatFsReaderWriter
10
+
11
+ from . import _utils, _export
12
+
13
+ __all__ = export = _export.Export() # type: ignore
14
+
15
+
16
+ @export
17
+ class LocalFlatFs:
18
+ """FlatFS implementation using local filesystem to store files.
19
+
20
+ You can use this to access files from an existing directory (the paths are
21
+ then relative to *root_dir*), or to create a completely new filesystem
22
+ inside an empty directory.
23
+
24
+ :param root_dir:
25
+ The path to the root directory where files will be stored.
26
+
27
+ :param exclude:
28
+ The glob patterns of paths to exclude.
29
+
30
+ For example, you may want to exclude ``.git`` directory when you use
31
+ FlatFS to access content of some Git repository.
32
+
33
+ For more details on pattern syntax used check :mod:`fnmatch` which is
34
+ used underneath.
35
+
36
+ :param include:
37
+ The glob patterns of paths to include.
38
+
39
+ These can only be paths from inside root directory. For example, you
40
+ may wish to allow just ``src`` and ``test`` dirs, not all files.
41
+
42
+ For more details on pattern syntax used check :mod:`fnmatch` which is
43
+ used underneath.
44
+ """
45
+
46
+ def __init__(self, root_dir: pathlib.Path, exclude: Optional[set[str]] = None, include: Optional[set[str]] = None):
47
+ self.__root_dir = root_dir
48
+ self.__exclude = exclude or set()
49
+ self.__include = include or set()
50
+
51
+ def __is_excluded(self, normalized_path: str) -> bool:
52
+ for pattern in self.__exclude:
53
+ if fnmatch.fnmatch(normalized_path, pattern):
54
+ return True
55
+ return False
56
+
57
+ def __is_included(self, normalized_path: str) -> bool:
58
+ if not self.__include:
59
+ return True
60
+ for pattern in self.__include:
61
+ if fnmatch.fnmatch(normalized_path, pattern):
62
+ return True
63
+ return False
64
+
65
+ def __make_abspath(self, path: str) -> pathlib.Path:
66
+ normalized_path = _utils.normalize_path(path)
67
+ if self.__is_excluded(normalized_path):
68
+ raise PathAccessError(path)
69
+ return self.__root_dir / normalized_path[1:]
70
+
71
+ def __walk(self, path: pathlib.Path, relpath: str):
72
+ for name in os.listdir(path):
73
+ fullpath = path / name
74
+ fullrelpath = f"{relpath}/{name}"
75
+ if fullpath.is_dir():
76
+ yield from self.__walk(fullpath, fullrelpath)
77
+ elif not self.__is_excluded(fullrelpath) and self.__is_included(fullrelpath):
78
+ yield fullrelpath
79
+
80
+ def scan(self) -> Iterator[str]:
81
+ return self.__walk(self.__root_dir, "")
82
+
83
+ def exists(self, path: str) -> bool:
84
+ abspath = self.__make_abspath(path)
85
+ return abspath.is_file()
86
+
87
+ def read_chunks(self, path: str, chunk_size: int = 65535) -> Iterator[bytes]:
88
+ abspath = self.__make_abspath(path)
89
+ if not abspath.is_file():
90
+ raise PathNotFoundError(path)
91
+ with abspath.open("rb") as fd:
92
+ while True:
93
+ chunk = fd.read(chunk_size)
94
+ if not chunk:
95
+ break
96
+ yield chunk
97
+
98
+ def write_chunks(self, path: str, chunks: Iterable[bytes]):
99
+ abspath = self.__make_abspath(path)
100
+ abspath.parent.mkdir(parents=True, exist_ok=True)
101
+ with abspath.open("wb") as fd:
102
+ for chunk in chunks:
103
+ fd.write(chunk)
104
+
105
+ def remove(self, path: str):
106
+ abspath = self.__make_abspath(path)
107
+ if not abspath.is_file():
108
+ raise PathNotFoundError(path)
109
+ abspath.unlink()
110
+
111
+
112
+ @export
113
+ class InMemoryFlatFs:
114
+ """In-memory FlatFS implementation.
115
+
116
+ This is mostly suitable for use during testing to avoid unnecessary I/O and
117
+ disk usage.
118
+ """
119
+
120
+ def __init__(self) -> None:
121
+ self.__storage: dict[str, bytes] = {}
122
+
123
+ def __make_key(self, path: str) -> str:
124
+ normalized_key = _utils.normalize_path(path)
125
+ return normalized_key
126
+
127
+ def scan(self) -> Iterator[str]:
128
+ return iter(self.__storage.keys())
129
+
130
+ def exists(self, path: str) -> bool:
131
+ return self.__make_key(path) in self.__storage
132
+
133
+ def read_chunks(self, path: str, chunk_size: int = 65535) -> Iterator[bytes]:
134
+ key = self.__make_key(path)
135
+ if key not in self.__storage:
136
+ raise PathNotFoundError(path)
137
+ data_left = self.__storage[key]
138
+ while len(data_left) > 0:
139
+ chunk = data_left[:chunk_size]
140
+ data_left = data_left[chunk_size:]
141
+ yield chunk
142
+
143
+ def write_chunks(self, path: str, chunks: Iterable[bytes]):
144
+ self.__storage[self.__make_key(path)] = b"".join(chunks)
145
+
146
+ def remove(self, path: str):
147
+ key = self.__make_key(path)
148
+ if key not in self.__storage:
149
+ raise PathNotFoundError(path)
150
+ del self.__storage[key]
151
+
152
+
153
+ @export
154
+ class AsyncFlatFsAdapter:
155
+ """Adapter that wraps given :class:`flatfs.interface.FlatFsReaderWriter`
156
+ with async interface.
157
+
158
+ You can use this to adapt e.g. :class:`LocalFlatFs` or
159
+ :class:`InMemoryFlatFs` objects with async interface allowing for
160
+ convenient use with async code. The blocking calls are dispatched to
161
+ background worker which communicates with the caller using queues.
162
+
163
+ :param target:
164
+ The target FlatFS reader-writer to wrap.
165
+ """
166
+
167
+ def __init__(self, target: FlatFsReaderWriter):
168
+ self.__target = target
169
+
170
+ async def scan(self) -> AsyncGenerator[str, None]:
171
+
172
+ def scanner():
173
+ for path in self.__target.scan():
174
+ loop.call_soon_threadsafe(queue.put_nowait, path)
175
+ loop.call_soon_threadsafe(queue.put_nowait, "")
176
+
177
+ loop = asyncio.get_running_loop()
178
+ asyncio.create_task(_utils.run_blocking(scanner))
179
+ queue: asyncio.Queue[str] = asyncio.Queue()
180
+ while True:
181
+ path = await queue.get()
182
+ if not path:
183
+ break
184
+ yield path
185
+
186
+ async def exists(self, path: str) -> bool:
187
+ return await _utils.run_blocking(self.__target.exists, path)
188
+
189
+ async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
190
+
191
+ def reader():
192
+ try:
193
+ for chunk in self.__target.read_chunks(path, chunk_size):
194
+ loop.call_soon_threadsafe(queue.put_nowait, chunk)
195
+ loop.call_soon_threadsafe(queue.put_nowait, b"")
196
+ except Exception as e:
197
+ loop.call_soon_threadsafe(queue.put_nowait, e)
198
+
199
+ loop = asyncio.get_running_loop()
200
+ queue: asyncio.Queue[bytes | Exception] = asyncio.Queue()
201
+ asyncio.create_task(_utils.run_blocking(reader))
202
+ while True:
203
+ chunk_or_exc = await queue.get()
204
+ if not chunk_or_exc:
205
+ break
206
+ if isinstance(chunk_or_exc, Exception):
207
+ raise chunk_or_exc
208
+ yield chunk_or_exc
209
+
210
+ async def write_chunks(self, path: str, chunks: AsyncIterable[bytes]):
211
+
212
+ def gen() -> Generator[bytes, None, None]:
213
+ while True:
214
+ chunk = queue.get()
215
+ if not chunk:
216
+ return
217
+ yield chunk
218
+
219
+ queue: Queue[bytes] = Queue()
220
+ task = asyncio.create_task(_utils.run_blocking(self.__target.write_chunks, path, gen()))
221
+ async for chunk in chunks:
222
+ queue.put(chunk)
223
+ queue.put(b"")
224
+ await task
225
+
226
+ async def remove(self, path: str):
227
+ await _utils.run_blocking(self.__target.remove, path)
@@ -0,0 +1,42 @@
1
+ from typing import Optional
2
+
3
+ from . import _export
4
+
5
+ __all__ = export = _export.Export() # type: ignore
6
+
7
+
8
+ @export
9
+ class FlatFsError(Exception):
10
+ """Base class for exceptions raised by this library."""
11
+
12
+ #: Message template or ``None`` to use default ``__str__`` implementation.
13
+ __message_template__: Optional[str] = None
14
+
15
+ def __str__(self) -> str:
16
+ if self.__message_template__ is None:
17
+ return super().__str__()
18
+ return self.__message_template__.format(self=self)
19
+
20
+
21
+ @export
22
+ class PathError(FlatFsError):
23
+ """Base class for path-related errors."""
24
+
25
+ __message_template__ = "{self.path}"
26
+
27
+ #: Path that caused error.
28
+ path: str
29
+
30
+ def __init__(self, path: str):
31
+ super().__init__()
32
+ self.path = path
33
+
34
+
35
+ @export
36
+ class PathNotFoundError(PathError):
37
+ """Raised when file was not found."""
38
+
39
+
40
+ @export
41
+ class PathAccessError(PathError):
42
+ """Raised when file that is tried to be accessed lays beyond filesystem bounds."""
@@ -0,0 +1,124 @@
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):
33
+ """Write given data to file at given path either creating the file, or
34
+ replacing with new content.
35
+
36
+ :param fs:
37
+ FlatFS object to write to.
38
+
39
+ :param path:
40
+ The path to a file to create or replace.
41
+
42
+ :param data:
43
+ The data to write.
44
+
45
+ :param chunk_size:
46
+ The chunk size to use during writing.
47
+
48
+ If *data* length is greater, it will be split into chunks and written
49
+ chunk by chunk, with each chunk having at most *chunk_size* bytes.
50
+
51
+ Defaults to 64kB.
52
+ """
53
+
54
+ def gen():
55
+ data_left = data
56
+ while True:
57
+ chunk = data_left[:chunk_size]
58
+ if not chunk:
59
+ break
60
+ data_left = data_left[chunk_size:]
61
+ yield chunk
62
+
63
+ fs.write_chunks(path, gen())
64
+
65
+
66
+ @export
67
+ def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535) -> str:
68
+ """Read whole text file and return as string.
69
+
70
+ Uses :func:`read_bytes` underneath and then just decodes its output using
71
+ provided *encoding*, which is ``utf-8`` by default.
72
+ """
73
+ return read_bytes(fs, path, chunk_size).decode(encoding)
74
+
75
+
76
+ @export
77
+ def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535):
78
+ """Create new text file or replace existing with given text data.
79
+
80
+ Uses :func:`write_bytes` underneath and just encodes given *data* with
81
+ provided *encoding*, which is ``utf-8`` by default.
82
+ """
83
+ return write_bytes(fs, path, data.encode(encoding), chunk_size)
84
+
85
+
86
+ @export
87
+ async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: int = 65535) -> bytes:
88
+ """Same as :func:`read_bytes`, but for async code."""
89
+ out = b""
90
+ async for chunk in fs.read_chunks(path, chunk_size):
91
+ out += chunk
92
+ return out
93
+
94
+
95
+ @export
96
+ async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535):
97
+ """Same as :func:`write_bytes`, but for async code."""
98
+
99
+ async def gen():
100
+ data_left = data
101
+ while True:
102
+ chunk = data_left[:chunk_size]
103
+ if not chunk:
104
+ break
105
+ data_left = data_left[chunk_size:]
106
+ yield chunk
107
+
108
+ await fs.write_chunks(path, gen())
109
+
110
+
111
+ @export
112
+ async def async_read_text(
113
+ fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535
114
+ ) -> str:
115
+ """Same as :func:`read_text`, but for async code."""
116
+ return (await async_read_bytes(fs, path, chunk_size)).decode(encoding)
117
+
118
+
119
+ @export
120
+ async def async_write_text(
121
+ fs: SupportsAsyncWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535
122
+ ):
123
+ """Same as :func:`write_text`, but for async code."""
124
+ await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
@@ -0,0 +1,149 @@
1
+ from typing import AsyncGenerator, AsyncIterable, Iterable, Iterator, Protocol
2
+
3
+ from . import _export
4
+
5
+ __all__ = export = _export.Export() # type: ignore
6
+
7
+
8
+ @export
9
+ class SupportsReadChunks(Protocol):
10
+ """Protocol for objects that support chunked reading."""
11
+
12
+ def read_chunks(self, path: str, chunk_size: int = 65535) -> Iterator[bytes]:
13
+ """Read file in chunks and return iterator yielding chunks.
14
+
15
+ This method can be safely used with large files without the risk of
16
+ getting out of memory.
17
+
18
+ If *path* does not point to an existing file,
19
+ :exc:`flatfs.exc.PathNotFoundError` will be raised.
20
+
21
+ :param path:
22
+ The path to a file to read.
23
+
24
+ :param chunk_size:
25
+ The maximum chunk size (in bytes).
26
+
27
+ This is the maximum length of a single generated bytes object.
28
+ """
29
+ ...
30
+
31
+
32
+ @export
33
+ class SupportsWriteChunks(Protocol):
34
+ """Protocol for object that support writing in chunks."""
35
+
36
+ def write_chunks(self, path: str, chunks: Iterable[bytes]):
37
+ """Create or overwrite file at given *path* and write chunks of data
38
+ generated by provided *chunks* iterable.
39
+
40
+ :param path:
41
+ The path to a file to create or modify.
42
+
43
+ :param chunks:
44
+ Iterable providing chunks of bytes to write to a file.
45
+ """
46
+ ...
47
+
48
+
49
+ @export
50
+ class FlatFsReader(SupportsReadChunks, Protocol):
51
+ """Read-only interface to access files in the flat filesystem."""
52
+
53
+ def scan(self) -> Iterator[str]:
54
+ """Scan filesystem and generate paths to existing files.
55
+
56
+ Each yielded path can later be used with other methods of this
57
+ interface.
58
+ """
59
+ ...
60
+
61
+ def exists(self, path: str) -> bool:
62
+ """Check if *path* points to an existing file."""
63
+ ...
64
+
65
+
66
+ @export
67
+ class FlatFsReaderWriter(FlatFsReader, SupportsWriteChunks, Protocol):
68
+ """Read-write interface to access files in the flat filesystem."""
69
+
70
+ def remove(self, path: str):
71
+ """Remove file at given *path*.
72
+
73
+ Raises :exc:`flatfs.exc.PathNotFoundError` if there was no file to
74
+ remove at given *path*.
75
+
76
+ :param path:
77
+ The path to a file to remove.
78
+ """
79
+ ...
80
+
81
+
82
+ @export
83
+ class SupportsAsyncReadChunks(Protocol):
84
+ def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
85
+ """Read file in chunks and return iterator yielding chunks.
86
+
87
+ This method can be safely used with large files without the risk of
88
+ getting out of memory.
89
+
90
+ If *path* does not point to an existing file,
91
+ :exc:`flatfs.exc.PathNotFoundError` will be raised.
92
+
93
+ :param path:
94
+ The path to a file to read.
95
+
96
+ :param chunk_size:
97
+ The maximum chunk size (in bytes).
98
+
99
+ This is the maximum length of a single generated bytes object.
100
+ """
101
+ ...
102
+
103
+
104
+ @export
105
+ class SupportsAsyncWriteChunks(Protocol):
106
+ async def write_chunks(self, path: str, chunks: AsyncIterable[bytes]):
107
+ """Create or overwrite file at given *path* and write chunks of data
108
+ generated by provided *chunks* iterable.
109
+
110
+ :param path:
111
+ The path to a file to create or modify.
112
+
113
+ :param chunks:
114
+ Iterable providing chunks of bytes to write to a file.
115
+ """
116
+ ...
117
+
118
+
119
+ @export
120
+ class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
121
+ """Async read-only interface to access files in the flat filesystem."""
122
+
123
+ def scan(self) -> AsyncGenerator[str, None]:
124
+ """Scan filesystem and generate paths to existing files.
125
+
126
+ Each yielded path can later be used with other methods of this
127
+ interface.
128
+ """
129
+ ...
130
+
131
+ async def exists(self, path: str) -> bool:
132
+ """Check if *path* points to an existing file."""
133
+ ...
134
+
135
+
136
+ @export
137
+ class AsyncFlatFsReaderWriter(AsyncFlatFsReader, SupportsAsyncWriteChunks, Protocol):
138
+ """Async read-write interface to access files in the flat filesystem."""
139
+
140
+ async def remove(self, path: str):
141
+ """Remove file at given *path*.
142
+
143
+ Raises :exc:`flatfs.exc.PathNotFoundError` if there was no file to
144
+ remove at given *path*.
145
+
146
+ :param path:
147
+ The path to a file to remove.
148
+ """
149
+ ...
@@ -0,0 +1,37 @@
1
+ [project]
2
+ name = "flatfs"
3
+ version = "0.0.0"
4
+ description = "Minimal filesystem abstraction with pluggable backends, treating paths as keys and file contents as values."
5
+ authors = [
6
+ {name = "Maciej Wiatrzyk", email = "maciej.wiatrzyk@gmail.com"}
7
+ ]
8
+ license = {text = "MIT"}
9
+ readme = "README.md"
10
+ keywords = ["flat", "filesystem", "abstraction", "library"]
11
+ classifiers = [
12
+ "Development Status :: 4 - Beta",
13
+ #"Development Status :: 5 - Production/Stable" <- Use this for first 1.x version
14
+ "Intended Audience :: Developers",
15
+ "Operating System :: OS Independent",
16
+ "Topic :: Software Development :: Libraries",
17
+ ]
18
+ requires-python = ">=3.10,<4"
19
+ dependencies = [
20
+ ]
21
+
22
+ [project.urls]
23
+ repository = "https://github.com/mwiatrzyk/flatfs"
24
+ "Bug Tracker" = "https://github.com/mwiatrzyk/flatfs/issues"
25
+
26
+ [build-system]
27
+ requires = ["poetry-core>=2.0.0,<3.0.0"]
28
+ build-backend = "poetry.core.masonry.api"
29
+
30
+ [tool.poetry.group.dev.dependencies]
31
+ pytest = "^9.0.3"
32
+ invoke = "^3.0.3"
33
+ pytest-asyncio = "^1.3.0"
34
+ mockify = "^0.14.0"
35
+ pytest-cov = "^7.1.0"
36
+ ruff = "^0.15.12"
37
+ mypy = "^2.0.0"