flatfs 0.0.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.
- {flatfs-0.0.0 → flatfs-0.3.0}/PKG-INFO +3 -2
- flatfs-0.3.0/flatfs/__init__.py +1 -0
- flatfs-0.3.0/flatfs/_utils.py +110 -0
- {flatfs-0.0.0 → flatfs-0.3.0}/flatfs/backends.py +63 -25
- flatfs-0.3.0/flatfs/helpers.py +449 -0
- flatfs-0.3.0/flatfs/interface.py +198 -0
- {flatfs-0.0.0 → flatfs-0.3.0}/pyproject.toml +2 -1
- flatfs-0.0.0/flatfs/__init__.py +0 -0
- flatfs-0.0.0/flatfs/_utils.py +0 -52
- flatfs-0.0.0/flatfs/helpers.py +0 -124
- flatfs-0.0.0/flatfs/interface.py +0 -149
- {flatfs-0.0.0 → flatfs-0.3.0}/LICENSE.txt +0 -0
- {flatfs-0.0.0 → flatfs-0.3.0}/README.md +0 -0
- {flatfs-0.0.0 → flatfs-0.3.0}/flatfs/_export.py +0 -0
- {flatfs-0.0.0 → flatfs-0.3.0}/flatfs/api.py +0 -0
- {flatfs-0.0.0 → flatfs-0.3.0}/flatfs/exc.py +0 -0
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
2
|
Name: flatfs
|
|
3
|
-
Version: 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
|
+
License-File: LICENSE.txt
|
|
6
7
|
Keywords: flat,filesystem,abstraction,library
|
|
7
8
|
Author: Maciej Wiatrzyk
|
|
8
9
|
Author-email: maciej.wiatrzyk@gmail.com
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.3.0"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import datetime
|
|
3
|
+
import functools
|
|
4
|
+
import queue
|
|
5
|
+
from typing import AsyncGenerator, Callable, Generator, TypeVar
|
|
6
|
+
|
|
7
|
+
from flatfs.exc import PathAccessError
|
|
8
|
+
|
|
9
|
+
T = TypeVar("T")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def normalize_path(path: str) -> str:
|
|
13
|
+
"""Normalizes path by removing duplicated slash characters and by
|
|
14
|
+
evaluating relative path components (e.g. ``../`` or ``./``).
|
|
15
|
+
|
|
16
|
+
Returns normalized path, or raises :exc:`flatfs.exc.FileAccessError` if
|
|
17
|
+
final path would go beyond filesystem bounds.
|
|
18
|
+
|
|
19
|
+
:param path:
|
|
20
|
+
The path to normalize.
|
|
21
|
+
"""
|
|
22
|
+
parts: list[str] = []
|
|
23
|
+
for part in path.split("/"):
|
|
24
|
+
if not part or part == ".":
|
|
25
|
+
continue
|
|
26
|
+
if part == "..":
|
|
27
|
+
if not parts:
|
|
28
|
+
raise PathAccessError(path)
|
|
29
|
+
parts.pop()
|
|
30
|
+
else:
|
|
31
|
+
parts.append(part)
|
|
32
|
+
out = "/".join(parts)
|
|
33
|
+
if out[0] == "/":
|
|
34
|
+
return out
|
|
35
|
+
return "/" + out
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
async def run_blocking(func: Callable[..., T], *args, **kwargs) -> T:
|
|
39
|
+
"""Run blocking (i.e. non-async) task in an event loop's default
|
|
40
|
+
executor.
|
|
41
|
+
|
|
42
|
+
On success, return *func*'s result, on failure raise *func*'s exception.
|
|
43
|
+
|
|
44
|
+
:param func:
|
|
45
|
+
The non-async callable to run.
|
|
46
|
+
|
|
47
|
+
:param `*args`:
|
|
48
|
+
Positional args to be passed to *func*.
|
|
49
|
+
|
|
50
|
+
:param `**kwargs`:
|
|
51
|
+
Named args to be passed to *func*.
|
|
52
|
+
"""
|
|
53
|
+
wrapped_func = functools.partial(func, *args, **kwargs)
|
|
54
|
+
return await asyncio.get_running_loop().run_in_executor(None, wrapped_func)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def utcnow() -> datetime.datetime:
|
|
58
|
+
"""Get the current date and time in UTC."""
|
|
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
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import dataclasses
|
|
3
|
+
import datetime
|
|
2
4
|
import fnmatch
|
|
3
5
|
import os
|
|
4
6
|
import pathlib
|
|
5
7
|
from queue import Queue
|
|
6
|
-
from typing import AsyncGenerator,
|
|
8
|
+
from typing import AsyncGenerator, AsyncIterator, Generator, Iterator, Optional
|
|
7
9
|
|
|
8
10
|
from flatfs.exc import PathAccessError, PathNotFoundError
|
|
9
|
-
from flatfs.interface import FlatFsReaderWriter
|
|
11
|
+
from flatfs.interface import FlatFsReaderWriter, Stat
|
|
10
12
|
|
|
11
13
|
from . import _utils, _export
|
|
12
14
|
|
|
@@ -80,11 +82,21 @@ class LocalFlatFs:
|
|
|
80
82
|
def scan(self) -> Iterator[str]:
|
|
81
83
|
return self.__walk(self.__root_dir, "")
|
|
82
84
|
|
|
85
|
+
def stat(self, path: str) -> Stat:
|
|
86
|
+
abspath = self.__make_abspath(path)
|
|
87
|
+
if not abspath.is_file():
|
|
88
|
+
raise PathNotFoundError(path)
|
|
89
|
+
stat_result = abspath.stat()
|
|
90
|
+
return Stat(
|
|
91
|
+
modified=datetime.datetime.fromtimestamp(stat_result.st_mtime, datetime.timezone.utc),
|
|
92
|
+
size=stat_result.st_size,
|
|
93
|
+
)
|
|
94
|
+
|
|
83
95
|
def exists(self, path: str) -> bool:
|
|
84
96
|
abspath = self.__make_abspath(path)
|
|
85
97
|
return abspath.is_file()
|
|
86
98
|
|
|
87
|
-
def read_chunks(self, path: str, chunk_size: int = 65535) ->
|
|
99
|
+
def read_chunks(self, path: str, chunk_size: int = 65535) -> Generator[bytes, None, None]:
|
|
88
100
|
abspath = self.__make_abspath(path)
|
|
89
101
|
if not abspath.is_file():
|
|
90
102
|
raise PathNotFoundError(path)
|
|
@@ -95,12 +107,12 @@ class LocalFlatFs:
|
|
|
95
107
|
break
|
|
96
108
|
yield chunk
|
|
97
109
|
|
|
98
|
-
def write_chunks(self, path: str,
|
|
110
|
+
def write_chunks(self, path: str, chunk_gen: Generator[bytes, None, None]) -> Generator[int, None, None]:
|
|
99
111
|
abspath = self.__make_abspath(path)
|
|
100
112
|
abspath.parent.mkdir(parents=True, exist_ok=True)
|
|
101
113
|
with abspath.open("wb") as fd:
|
|
102
|
-
for chunk in
|
|
103
|
-
fd.write(chunk)
|
|
114
|
+
for chunk in chunk_gen:
|
|
115
|
+
yield fd.write(chunk)
|
|
104
116
|
|
|
105
117
|
def remove(self, path: str):
|
|
106
118
|
abspath = self.__make_abspath(path)
|
|
@@ -117,8 +129,13 @@ class InMemoryFlatFs:
|
|
|
117
129
|
disk usage.
|
|
118
130
|
"""
|
|
119
131
|
|
|
132
|
+
@dataclasses.dataclass
|
|
133
|
+
class _File:
|
|
134
|
+
payload: bytes
|
|
135
|
+
stat: Stat
|
|
136
|
+
|
|
120
137
|
def __init__(self) -> None:
|
|
121
|
-
self.__storage: dict[str,
|
|
138
|
+
self.__storage: dict[str, InMemoryFlatFs._File] = {}
|
|
122
139
|
|
|
123
140
|
def __make_key(self, path: str) -> str:
|
|
124
141
|
normalized_key = _utils.normalize_path(path)
|
|
@@ -127,21 +144,37 @@ class InMemoryFlatFs:
|
|
|
127
144
|
def scan(self) -> Iterator[str]:
|
|
128
145
|
return iter(self.__storage.keys())
|
|
129
146
|
|
|
147
|
+
def stat(self, path: str) -> Stat:
|
|
148
|
+
key = self.__make_key(path)
|
|
149
|
+
if key not in self.__storage:
|
|
150
|
+
raise PathNotFoundError(path)
|
|
151
|
+
return self.__storage[key].stat
|
|
152
|
+
|
|
130
153
|
def exists(self, path: str) -> bool:
|
|
131
154
|
return self.__make_key(path) in self.__storage
|
|
132
155
|
|
|
133
|
-
def read_chunks(self, path: str, chunk_size: int = 65535) ->
|
|
156
|
+
def read_chunks(self, path: str, chunk_size: int = 65535) -> Generator[bytes, None, None]:
|
|
134
157
|
key = self.__make_key(path)
|
|
135
158
|
if key not in self.__storage:
|
|
136
159
|
raise PathNotFoundError(path)
|
|
137
|
-
data_left = self.__storage[key]
|
|
160
|
+
data_left = self.__storage[key].payload
|
|
138
161
|
while len(data_left) > 0:
|
|
139
162
|
chunk = data_left[:chunk_size]
|
|
140
163
|
data_left = data_left[chunk_size:]
|
|
141
164
|
yield chunk
|
|
142
165
|
|
|
143
|
-
def write_chunks(self, path: str,
|
|
144
|
-
|
|
166
|
+
def write_chunks(self, path: str, chunk_gen: Generator[bytes, None, None]) -> Generator[int, None, None]:
|
|
167
|
+
now = _utils.utcnow()
|
|
168
|
+
stat = Stat(
|
|
169
|
+
modified=now,
|
|
170
|
+
size=0,
|
|
171
|
+
)
|
|
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
|
|
145
178
|
|
|
146
179
|
def remove(self, path: str):
|
|
147
180
|
key = self.__make_key(path)
|
|
@@ -186,7 +219,10 @@ class AsyncFlatFsAdapter:
|
|
|
186
219
|
async def exists(self, path: str) -> bool:
|
|
187
220
|
return await _utils.run_blocking(self.__target.exists, path)
|
|
188
221
|
|
|
189
|
-
async def
|
|
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]:
|
|
190
226
|
|
|
191
227
|
def reader():
|
|
192
228
|
try:
|
|
@@ -207,21 +243,23 @@ class AsyncFlatFsAdapter:
|
|
|
207
243
|
raise chunk_or_exc
|
|
208
244
|
yield chunk_or_exc
|
|
209
245
|
|
|
210
|
-
async def write_chunks(self, path: str,
|
|
246
|
+
async def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
|
|
211
247
|
|
|
212
|
-
def
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if not chunk:
|
|
216
|
-
return
|
|
217
|
-
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)
|
|
218
251
|
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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
|
|
225
263
|
|
|
226
264
|
async def remove(self, path: str):
|
|
227
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()
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
from typing import AsyncGenerator, AsyncIterator, Generator, Iterator, Protocol
|
|
4
|
+
|
|
5
|
+
from . import _export
|
|
6
|
+
|
|
7
|
+
__all__ = export = _export.Export() # type: ignore
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@export
|
|
11
|
+
@dataclasses.dataclass
|
|
12
|
+
class Stat:
|
|
13
|
+
"""Object containing file stats."""
|
|
14
|
+
|
|
15
|
+
#: The file last modification date and time (in UTC).
|
|
16
|
+
modified: datetime.datetime
|
|
17
|
+
|
|
18
|
+
#: The file size in bytes.
|
|
19
|
+
size: int
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@export
|
|
23
|
+
class SupportsReadChunks(Protocol):
|
|
24
|
+
"""Protocol for objects that support chunked reading."""
|
|
25
|
+
|
|
26
|
+
def read_chunks(self, path: str, chunk_size: int = 65535) -> Generator[bytes, None, None]:
|
|
27
|
+
"""Return generator for reading existing file in chunks of given
|
|
28
|
+
maximum size.
|
|
29
|
+
|
|
30
|
+
If *path* does not point to an existing file
|
|
31
|
+
:exc:`flatfs.exc.PathNotFoundError` will be raised.
|
|
32
|
+
|
|
33
|
+
Closing or expiring the returned generator automatically closes
|
|
34
|
+
underlying file.
|
|
35
|
+
|
|
36
|
+
:param path:
|
|
37
|
+
The path to a file to read.
|
|
38
|
+
|
|
39
|
+
:param chunk_size:
|
|
40
|
+
The maximum chunk size (in bytes).
|
|
41
|
+
|
|
42
|
+
This is the maximum length of a single generated bytes object.
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@export
|
|
48
|
+
class SupportsWriteChunks(Protocol):
|
|
49
|
+
"""Protocol for object that support writing in chunks."""
|
|
50
|
+
|
|
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.
|
|
57
|
+
|
|
58
|
+
Closing or expiring returned generator automatically closes underlying
|
|
59
|
+
file and flushes what was written so far.
|
|
60
|
+
|
|
61
|
+
:param path:
|
|
62
|
+
The path to a file.
|
|
63
|
+
|
|
64
|
+
:param chunk_gen:
|
|
65
|
+
Generator producing chunks to be written.
|
|
66
|
+
"""
|
|
67
|
+
...
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@export
|
|
71
|
+
class FlatFsReader(SupportsReadChunks, Protocol):
|
|
72
|
+
"""Read-only interface to access files in the flat filesystem."""
|
|
73
|
+
|
|
74
|
+
def scan(self) -> Iterator[str]:
|
|
75
|
+
"""Return iterator that yields normalized paths to existing files."""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
def stat(self, path: str) -> Stat:
|
|
79
|
+
"""Get stats of a file at given path.
|
|
80
|
+
|
|
81
|
+
This can be used to retrieve file size, file modification time etc.
|
|
82
|
+
|
|
83
|
+
If the file does not exist, :exc:`flatfs.exc.PathNotFoundError` will be
|
|
84
|
+
raised.
|
|
85
|
+
|
|
86
|
+
:param path:
|
|
87
|
+
The path to a file.
|
|
88
|
+
"""
|
|
89
|
+
...
|
|
90
|
+
|
|
91
|
+
def exists(self, path: str) -> bool:
|
|
92
|
+
"""Check if path points to an existing file.
|
|
93
|
+
|
|
94
|
+
:param path:
|
|
95
|
+
The path to a file.
|
|
96
|
+
"""
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@export
|
|
101
|
+
class FlatFsReaderWriter(FlatFsReader, SupportsWriteChunks, Protocol):
|
|
102
|
+
"""Read-write interface to access files in the flat filesystem."""
|
|
103
|
+
|
|
104
|
+
def remove(self, path: str):
|
|
105
|
+
"""Remove existing file at given path.
|
|
106
|
+
|
|
107
|
+
Raises :exc:`flatfs.exc.PathNotFoundError` if there was no file to
|
|
108
|
+
remove at given path.
|
|
109
|
+
|
|
110
|
+
:param path:
|
|
111
|
+
The path to a file to remove.
|
|
112
|
+
"""
|
|
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
|
+
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"}
|
|
@@ -35,3 +35,4 @@ mockify = "^0.14.0"
|
|
|
35
35
|
pytest-cov = "^7.1.0"
|
|
36
36
|
ruff = "^0.15.12"
|
|
37
37
|
mypy = "^2.0.0"
|
|
38
|
+
bumpify = "^0.5.1"
|
flatfs-0.0.0/flatfs/__init__.py
DELETED
|
File without changes
|
flatfs-0.0.0/flatfs/_utils.py
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
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)
|
flatfs-0.0.0/flatfs/helpers.py
DELETED
|
@@ -1,124 +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):
|
|
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)
|
flatfs-0.0.0/flatfs/interface.py
DELETED
|
@@ -1,149 +0,0 @@
|
|
|
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
|
-
...
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|