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.
@@ -1,8 +1,9 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: flatfs
3
- Version: 0.0.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) -> Iterator[bytes]:
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, bytes] = {}
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) -> Iterator[bytes]:
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
- self.__storage[self.__make_key(path)] = b"".join(chunks)
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
- from typing import AsyncGenerator, AsyncIterable, Iterable, Iterator, Protocol
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) -> Iterator[bytes]:
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.0.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"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes