flatfs 0.0.0__tar.gz → 0.2.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.2.0}/PKG-INFO +3 -2
- flatfs-0.2.0/flatfs/__init__.py +1 -0
- {flatfs-0.0.0 → flatfs-0.2.0}/flatfs/_utils.py +6 -0
- {flatfs-0.0.0 → flatfs-0.2.0}/flatfs/backends.py +46 -11
- {flatfs-0.0.0 → flatfs-0.2.0}/flatfs/helpers.py +38 -4
- {flatfs-0.0.0 → flatfs-0.2.0}/flatfs/interface.py +51 -5
- {flatfs-0.0.0 → flatfs-0.2.0}/pyproject.toml +2 -1
- flatfs-0.0.0/flatfs/__init__.py +0 -0
- {flatfs-0.0.0 → flatfs-0.2.0}/LICENSE.txt +0 -0
- {flatfs-0.0.0 → flatfs-0.2.0}/README.md +0 -0
- {flatfs-0.0.0 → flatfs-0.2.0}/flatfs/_export.py +0 -0
- {flatfs-0.0.0 → flatfs-0.2.0}/flatfs/api.py +0 -0
- {flatfs-0.0.0 → flatfs-0.2.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.2.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.2.0"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import datetime
|
|
2
3
|
import functools
|
|
3
4
|
from typing import Callable, TypeVar
|
|
4
5
|
|
|
@@ -50,3 +51,8 @@ async def run_blocking(func: Callable[..., T], *args, **kwargs) -> T:
|
|
|
50
51
|
"""
|
|
51
52
|
wrapped_func = functools.partial(func, *args, **kwargs)
|
|
52
53
|
return await asyncio.get_running_loop().run_in_executor(None, wrapped_func)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def utcnow() -> datetime.datetime:
|
|
57
|
+
"""Get the current date and time in UTC."""
|
|
58
|
+
return datetime.datetime.now(datetime.timezone.utc)
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import dataclasses
|
|
3
|
+
import datetime
|
|
2
4
|
import fnmatch
|
|
3
5
|
import os
|
|
4
6
|
import pathlib
|
|
@@ -6,7 +8,7 @@ from queue import Queue
|
|
|
6
8
|
from typing import AsyncGenerator, AsyncIterable, Generator, Iterable, 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,14 @@ class LocalFlatFs:
|
|
|
95
107
|
break
|
|
96
108
|
yield chunk
|
|
97
109
|
|
|
98
|
-
def write_chunks(self, path: str, chunks: Iterable[bytes]):
|
|
110
|
+
def write_chunks(self, path: str, chunks: Iterable[bytes]) -> int:
|
|
99
111
|
abspath = self.__make_abspath(path)
|
|
100
112
|
abspath.parent.mkdir(parents=True, exist_ok=True)
|
|
113
|
+
total_bytes = 0
|
|
101
114
|
with abspath.open("wb") as fd:
|
|
102
115
|
for chunk in chunks:
|
|
103
|
-
fd.write(chunk)
|
|
116
|
+
total_bytes += fd.write(chunk)
|
|
117
|
+
return total_bytes
|
|
104
118
|
|
|
105
119
|
def remove(self, path: str):
|
|
106
120
|
abspath = self.__make_abspath(path)
|
|
@@ -117,8 +131,13 @@ class InMemoryFlatFs:
|
|
|
117
131
|
disk usage.
|
|
118
132
|
"""
|
|
119
133
|
|
|
134
|
+
@dataclasses.dataclass
|
|
135
|
+
class _File:
|
|
136
|
+
payload: bytes
|
|
137
|
+
stat: Stat
|
|
138
|
+
|
|
120
139
|
def __init__(self) -> None:
|
|
121
|
-
self.__storage: dict[str,
|
|
140
|
+
self.__storage: dict[str, InMemoryFlatFs._File] = {}
|
|
122
141
|
|
|
123
142
|
def __make_key(self, path: str) -> str:
|
|
124
143
|
normalized_key = _utils.normalize_path(path)
|
|
@@ -127,21 +146,34 @@ class InMemoryFlatFs:
|
|
|
127
146
|
def scan(self) -> Iterator[str]:
|
|
128
147
|
return iter(self.__storage.keys())
|
|
129
148
|
|
|
149
|
+
def stat(self, path: str) -> Stat:
|
|
150
|
+
key = self.__make_key(path)
|
|
151
|
+
if key not in self.__storage:
|
|
152
|
+
raise PathNotFoundError(path)
|
|
153
|
+
return self.__storage[key].stat
|
|
154
|
+
|
|
130
155
|
def exists(self, path: str) -> bool:
|
|
131
156
|
return self.__make_key(path) in self.__storage
|
|
132
157
|
|
|
133
|
-
def read_chunks(self, path: str, chunk_size: int = 65535) ->
|
|
158
|
+
def read_chunks(self, path: str, chunk_size: int = 65535) -> Generator[bytes, None, None]:
|
|
134
159
|
key = self.__make_key(path)
|
|
135
160
|
if key not in self.__storage:
|
|
136
161
|
raise PathNotFoundError(path)
|
|
137
|
-
data_left = self.__storage[key]
|
|
162
|
+
data_left = self.__storage[key].payload
|
|
138
163
|
while len(data_left) > 0:
|
|
139
164
|
chunk = data_left[:chunk_size]
|
|
140
165
|
data_left = data_left[chunk_size:]
|
|
141
166
|
yield chunk
|
|
142
167
|
|
|
143
|
-
def write_chunks(self, path: str, chunks: Iterable[bytes]):
|
|
144
|
-
|
|
168
|
+
def write_chunks(self, path: str, chunks: Iterable[bytes]) -> int:
|
|
169
|
+
payload = b"".join(chunks)
|
|
170
|
+
now = _utils.utcnow()
|
|
171
|
+
stat = Stat(
|
|
172
|
+
modified=now,
|
|
173
|
+
size=len(payload),
|
|
174
|
+
)
|
|
175
|
+
self.__storage[self.__make_key(path)] = self._File(payload, stat)
|
|
176
|
+
return stat.size
|
|
145
177
|
|
|
146
178
|
def remove(self, path: str):
|
|
147
179
|
key = self.__make_key(path)
|
|
@@ -186,6 +218,9 @@ class AsyncFlatFsAdapter:
|
|
|
186
218
|
async def exists(self, path: str) -> bool:
|
|
187
219
|
return await _utils.run_blocking(self.__target.exists, path)
|
|
188
220
|
|
|
221
|
+
async def stat(self, path: str) -> Stat:
|
|
222
|
+
return await _utils.run_blocking(self.__target.stat, path)
|
|
223
|
+
|
|
189
224
|
async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
|
|
190
225
|
|
|
191
226
|
def reader():
|
|
@@ -207,7 +242,7 @@ class AsyncFlatFsAdapter:
|
|
|
207
242
|
raise chunk_or_exc
|
|
208
243
|
yield chunk_or_exc
|
|
209
244
|
|
|
210
|
-
async def write_chunks(self, path: str, chunks: AsyncIterable[bytes]):
|
|
245
|
+
async def write_chunks(self, path: str, chunks: AsyncIterable[bytes]) -> int:
|
|
211
246
|
|
|
212
247
|
def gen() -> Generator[bytes, None, None]:
|
|
213
248
|
while True:
|
|
@@ -221,7 +256,7 @@ class AsyncFlatFsAdapter:
|
|
|
221
256
|
async for chunk in chunks:
|
|
222
257
|
queue.put(chunk)
|
|
223
258
|
queue.put(b"")
|
|
224
|
-
await task
|
|
259
|
+
return await task
|
|
225
260
|
|
|
226
261
|
async def remove(self, path: str):
|
|
227
262
|
await _utils.run_blocking(self.__target.remove, path)
|
|
@@ -29,10 +29,12 @@ def read_bytes(fs: SupportsReadChunks, path: str, chunk_size: int = 65535) -> by
|
|
|
29
29
|
|
|
30
30
|
|
|
31
31
|
@export
|
|
32
|
-
def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int = 65535):
|
|
32
|
+
def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
33
33
|
"""Write given data to file at given path either creating the file, or
|
|
34
34
|
replacing with new content.
|
|
35
35
|
|
|
36
|
+
Returns the number of bytes written.
|
|
37
|
+
|
|
36
38
|
:param fs:
|
|
37
39
|
FlatFS object to write to.
|
|
38
40
|
|
|
@@ -60,7 +62,7 @@ def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int
|
|
|
60
62
|
data_left = data_left[chunk_size:]
|
|
61
63
|
yield chunk
|
|
62
64
|
|
|
63
|
-
fs.write_chunks(path, gen())
|
|
65
|
+
return fs.write_chunks(path, gen())
|
|
64
66
|
|
|
65
67
|
|
|
66
68
|
@export
|
|
@@ -93,7 +95,7 @@ async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: i
|
|
|
93
95
|
|
|
94
96
|
|
|
95
97
|
@export
|
|
96
|
-
async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535):
|
|
98
|
+
async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
97
99
|
"""Same as :func:`write_bytes`, but for async code."""
|
|
98
100
|
|
|
99
101
|
async def gen():
|
|
@@ -105,7 +107,7 @@ async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes
|
|
|
105
107
|
data_left = data_left[chunk_size:]
|
|
106
108
|
yield chunk
|
|
107
109
|
|
|
108
|
-
await fs.write_chunks(path, gen())
|
|
110
|
+
return await fs.write_chunks(path, gen())
|
|
109
111
|
|
|
110
112
|
|
|
111
113
|
@export
|
|
@@ -122,3 +124,35 @@ async def async_write_text(
|
|
|
122
124
|
):
|
|
123
125
|
"""Same as :func:`write_text`, but for async code."""
|
|
124
126
|
await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
@export
|
|
130
|
+
class BinaryReader:
|
|
131
|
+
"""Helper for reading bytes from a file."""
|
|
132
|
+
|
|
133
|
+
def __init__(self, fs: SupportsReadChunks, path: str, chunk_size: int = 65535):
|
|
134
|
+
self.__chunk_gen = fs.read_chunks(path, chunk_size)
|
|
135
|
+
self.__current_chunk = b""
|
|
136
|
+
|
|
137
|
+
def read(self, count: int) -> bytes:
|
|
138
|
+
"""Read at most *count* bytes from an open file.
|
|
139
|
+
|
|
140
|
+
:param count:
|
|
141
|
+
The maximum number of bytes to read.
|
|
142
|
+
"""
|
|
143
|
+
if not self.__current_chunk:
|
|
144
|
+
self.__current_chunk = next(self.__chunk_gen, b"")
|
|
145
|
+
fragment = self.__current_chunk[:count]
|
|
146
|
+
self.__current_chunk = self.__current_chunk[count:]
|
|
147
|
+
return fragment
|
|
148
|
+
|
|
149
|
+
def close(self):
|
|
150
|
+
"""Close this reader.
|
|
151
|
+
|
|
152
|
+
This also closes the underlying file that is opened by flat filesystem
|
|
153
|
+
object given in the constructor.
|
|
154
|
+
|
|
155
|
+
This method is idempotent.
|
|
156
|
+
"""
|
|
157
|
+
self.__current_chunk = b""
|
|
158
|
+
self.__chunk_gen.close()
|
|
@@ -1,15 +1,29 @@
|
|
|
1
|
-
|
|
1
|
+
import dataclasses
|
|
2
|
+
import datetime
|
|
3
|
+
from typing import AsyncGenerator, AsyncIterable, Generator, Iterable, Iterator, Protocol
|
|
2
4
|
|
|
3
5
|
from . import _export
|
|
4
6
|
|
|
5
7
|
__all__ = export = _export.Export() # type: ignore
|
|
6
8
|
|
|
7
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
|
+
|
|
8
22
|
@export
|
|
9
23
|
class SupportsReadChunks(Protocol):
|
|
10
24
|
"""Protocol for objects that support chunked reading."""
|
|
11
25
|
|
|
12
|
-
def read_chunks(self, path: str, chunk_size: int = 65535) ->
|
|
26
|
+
def read_chunks(self, path: str, chunk_size: int = 65535) -> Generator[bytes, None, None]:
|
|
13
27
|
"""Read file in chunks and return iterator yielding chunks.
|
|
14
28
|
|
|
15
29
|
This method can be safely used with large files without the risk of
|
|
@@ -33,10 +47,13 @@ class SupportsReadChunks(Protocol):
|
|
|
33
47
|
class SupportsWriteChunks(Protocol):
|
|
34
48
|
"""Protocol for object that support writing in chunks."""
|
|
35
49
|
|
|
36
|
-
def write_chunks(self, path: str, chunks: Iterable[bytes]):
|
|
50
|
+
def write_chunks(self, path: str, chunks: Iterable[bytes]) -> int:
|
|
37
51
|
"""Create or overwrite file at given *path* and write chunks of data
|
|
38
52
|
generated by provided *chunks* iterable.
|
|
39
53
|
|
|
54
|
+
Returns the total number of bytes written, which is equal to the size
|
|
55
|
+
of a resulting file.
|
|
56
|
+
|
|
40
57
|
:param path:
|
|
41
58
|
The path to a file to create or modify.
|
|
42
59
|
|
|
@@ -58,8 +75,23 @@ class FlatFsReader(SupportsReadChunks, Protocol):
|
|
|
58
75
|
"""
|
|
59
76
|
...
|
|
60
77
|
|
|
78
|
+
def stat(self, path: str) -> Stat:
|
|
79
|
+
"""Get stats of a file at given *path*.
|
|
80
|
+
|
|
81
|
+
If the file does not exist, :exc:`flatfs.exc.PathNotFoundError` will be
|
|
82
|
+
raised.
|
|
83
|
+
|
|
84
|
+
:param path:
|
|
85
|
+
The path to a file.
|
|
86
|
+
"""
|
|
87
|
+
...
|
|
88
|
+
|
|
61
89
|
def exists(self, path: str) -> bool:
|
|
62
|
-
"""Check if *path* points to an existing file.
|
|
90
|
+
"""Check if *path* points to an existing file.
|
|
91
|
+
|
|
92
|
+
:param path:
|
|
93
|
+
The path to a file.
|
|
94
|
+
"""
|
|
63
95
|
...
|
|
64
96
|
|
|
65
97
|
|
|
@@ -103,10 +135,13 @@ class SupportsAsyncReadChunks(Protocol):
|
|
|
103
135
|
|
|
104
136
|
@export
|
|
105
137
|
class SupportsAsyncWriteChunks(Protocol):
|
|
106
|
-
async def write_chunks(self, path: str, chunks: AsyncIterable[bytes]):
|
|
138
|
+
async def write_chunks(self, path: str, chunks: AsyncIterable[bytes]) -> int:
|
|
107
139
|
"""Create or overwrite file at given *path* and write chunks of data
|
|
108
140
|
generated by provided *chunks* iterable.
|
|
109
141
|
|
|
142
|
+
Returns the total number of bytes written, which is equal to the size
|
|
143
|
+
of a resulting file.
|
|
144
|
+
|
|
110
145
|
:param path:
|
|
111
146
|
The path to a file to create or modify.
|
|
112
147
|
|
|
@@ -128,6 +163,17 @@ class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
|
|
|
128
163
|
"""
|
|
129
164
|
...
|
|
130
165
|
|
|
166
|
+
async def stat(self, path: str) -> Stat:
|
|
167
|
+
"""Get stats of a file at given *path*.
|
|
168
|
+
|
|
169
|
+
If the file does not exist, :exc:`flatfs.exc.PathNotFoundError` will be
|
|
170
|
+
raised.
|
|
171
|
+
|
|
172
|
+
:param path:
|
|
173
|
+
The path to a file.
|
|
174
|
+
"""
|
|
175
|
+
...
|
|
176
|
+
|
|
131
177
|
async def exists(self, path: str) -> bool:
|
|
132
178
|
"""Check if *path* points to an existing file."""
|
|
133
179
|
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flatfs"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|