flatfs 0.3.0__tar.gz → 0.5.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {flatfs-0.3.0 → flatfs-0.5.0}/PKG-INFO +2 -2
- flatfs-0.5.0/flatfs/__init__.py +1 -0
- flatfs-0.5.0/flatfs/_compat.py +17 -0
- flatfs-0.5.0/flatfs/aio/__init__.py +0 -0
- flatfs-0.5.0/flatfs/aio/api.py +15 -0
- flatfs-0.5.0/flatfs/aio/backends.py +90 -0
- flatfs-0.5.0/flatfs/aio/helpers.py +148 -0
- flatfs-0.5.0/flatfs/aio/interface.py +91 -0
- {flatfs-0.3.0 → flatfs-0.5.0}/flatfs/api.py +9 -1
- {flatfs-0.3.0 → flatfs-0.5.0}/flatfs/backends.py +2 -86
- {flatfs-0.3.0 → flatfs-0.5.0}/flatfs/helpers.py +6 -159
- {flatfs-0.3.0 → flatfs-0.5.0}/flatfs/interface.py +1 -86
- {flatfs-0.3.0 → flatfs-0.5.0}/pyproject.toml +5 -5
- flatfs-0.3.0/flatfs/__init__.py +0 -1
- {flatfs-0.3.0 → flatfs-0.5.0}/LICENSE.txt +0 -0
- {flatfs-0.3.0 → flatfs-0.5.0}/README.md +0 -0
- {flatfs-0.3.0 → flatfs-0.5.0}/flatfs/_export.py +0 -0
- {flatfs-0.3.0 → flatfs-0.5.0}/flatfs/_utils.py +0 -0
- {flatfs-0.3.0 → flatfs-0.5.0}/flatfs/exc.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flatfs
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Minimal filesystem abstraction with pluggable backends, treating paths as keys and file contents as values.
|
|
5
5
|
License: MIT
|
|
6
6
|
License-File: LICENSE.txt
|
|
7
7
|
Keywords: flat,filesystem,abstraction,library
|
|
8
8
|
Author: Maciej Wiatrzyk
|
|
9
9
|
Author-email: maciej.wiatrzyk@gmail.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9,<4
|
|
11
11
|
Classifier: Development Status :: 4 - Beta
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from typing import Any, Coroutine, Generic, Optional, Protocol, TypeVar
|
|
2
|
+
|
|
3
|
+
T = TypeVar("T")
|
|
4
|
+
Tco = TypeVar("Tco", covariant=True)
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SupportsAnext(Protocol, Generic[Tco]):
|
|
8
|
+
def __anext__(self) -> Coroutine[Any, Any, Tco]: ...
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
async def anext(obj: SupportsAnext[T], default: Optional[T] = None) -> T:
|
|
12
|
+
try:
|
|
13
|
+
return await obj.__anext__()
|
|
14
|
+
except StopAsyncIteration:
|
|
15
|
+
if default is not None:
|
|
16
|
+
return default
|
|
17
|
+
raise
|
|
File without changes
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""The all-in-one import helper for :mod:`flatfs.aio` module."""
|
|
2
|
+
|
|
3
|
+
from .backends import __all__ as _backends_all
|
|
4
|
+
from .helpers import __all__ as _helpers_all
|
|
5
|
+
from .interface import __all__ as _interface_all
|
|
6
|
+
|
|
7
|
+
from .backends import *
|
|
8
|
+
from .helpers import *
|
|
9
|
+
from .interface import *
|
|
10
|
+
|
|
11
|
+
__all__ = (
|
|
12
|
+
*_backends_all,
|
|
13
|
+
*_helpers_all,
|
|
14
|
+
*_interface_all,
|
|
15
|
+
) # type: ignore
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from queue import Queue
|
|
3
|
+
from typing import AsyncGenerator, AsyncIterator, Union
|
|
4
|
+
|
|
5
|
+
from flatfs import _export, _utils
|
|
6
|
+
from flatfs.interface import FlatFsReaderWriter, Stat
|
|
7
|
+
|
|
8
|
+
__all__ = export = _export.Export() # type: ignore
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@export
|
|
12
|
+
class AsyncFlatFsAdapter:
|
|
13
|
+
"""Adapter that wraps given :class:`flatfs.interface.FlatFsReaderWriter`
|
|
14
|
+
with async interface.
|
|
15
|
+
|
|
16
|
+
You can use this to adapt e.g. :class:`LocalFlatFs` or
|
|
17
|
+
:class:`InMemoryFlatFs` objects with async interface allowing for
|
|
18
|
+
convenient use with async code. The blocking calls are dispatched to
|
|
19
|
+
background worker which communicates with the caller using queues.
|
|
20
|
+
|
|
21
|
+
:param target:
|
|
22
|
+
The target FlatFS reader-writer to wrap.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
def __init__(self, target: FlatFsReaderWriter):
|
|
26
|
+
self.__target = target
|
|
27
|
+
|
|
28
|
+
async def scan(self) -> AsyncGenerator[str, None]:
|
|
29
|
+
|
|
30
|
+
def scanner():
|
|
31
|
+
for path in self.__target.scan():
|
|
32
|
+
loop.call_soon_threadsafe(queue.put_nowait, path)
|
|
33
|
+
loop.call_soon_threadsafe(queue.put_nowait, "")
|
|
34
|
+
|
|
35
|
+
loop = asyncio.get_running_loop()
|
|
36
|
+
asyncio.create_task(_utils.run_blocking(scanner))
|
|
37
|
+
queue: asyncio.Queue[str] = asyncio.Queue()
|
|
38
|
+
while True:
|
|
39
|
+
path = await queue.get()
|
|
40
|
+
if not path:
|
|
41
|
+
break
|
|
42
|
+
yield path
|
|
43
|
+
|
|
44
|
+
async def exists(self, path: str) -> bool:
|
|
45
|
+
return await _utils.run_blocking(self.__target.exists, path)
|
|
46
|
+
|
|
47
|
+
async def stat(self, path: str) -> Stat:
|
|
48
|
+
return await _utils.run_blocking(self.__target.stat, path)
|
|
49
|
+
|
|
50
|
+
async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncIterator[bytes]:
|
|
51
|
+
|
|
52
|
+
def reader():
|
|
53
|
+
try:
|
|
54
|
+
for chunk in self.__target.read_chunks(path, chunk_size):
|
|
55
|
+
loop.call_soon_threadsafe(queue.put_nowait, chunk)
|
|
56
|
+
loop.call_soon_threadsafe(queue.put_nowait, b"")
|
|
57
|
+
except Exception as e:
|
|
58
|
+
loop.call_soon_threadsafe(queue.put_nowait, e)
|
|
59
|
+
|
|
60
|
+
loop = asyncio.get_running_loop()
|
|
61
|
+
queue: asyncio.Queue[Union[bytes, Exception]] = asyncio.Queue()
|
|
62
|
+
asyncio.create_task(_utils.run_blocking(reader))
|
|
63
|
+
while True:
|
|
64
|
+
chunk_or_exc = await queue.get()
|
|
65
|
+
if not chunk_or_exc:
|
|
66
|
+
break
|
|
67
|
+
if isinstance(chunk_or_exc, Exception):
|
|
68
|
+
raise chunk_or_exc
|
|
69
|
+
yield chunk_or_exc
|
|
70
|
+
|
|
71
|
+
async def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
|
|
72
|
+
|
|
73
|
+
def blocking_writer():
|
|
74
|
+
for write_count in self.__target.write_chunks(path, _utils.generate_chunks_from_queue(chunk_queue)):
|
|
75
|
+
loop.call_soon_threadsafe(ack_queue.put_nowait, write_count)
|
|
76
|
+
|
|
77
|
+
loop = asyncio.get_running_loop()
|
|
78
|
+
chunk_queue: Queue[bytes] = Queue()
|
|
79
|
+
ack_queue: asyncio.Queue[int] = asyncio.Queue()
|
|
80
|
+
task = asyncio.create_task(_utils.run_blocking(blocking_writer))
|
|
81
|
+
try:
|
|
82
|
+
async for chunk in chunk_gen:
|
|
83
|
+
chunk_queue.put(chunk)
|
|
84
|
+
yield await ack_queue.get()
|
|
85
|
+
finally:
|
|
86
|
+
chunk_queue.put(b"")
|
|
87
|
+
await task
|
|
88
|
+
|
|
89
|
+
async def remove(self, path: str):
|
|
90
|
+
await _utils.run_blocking(self.__target.remove, path)
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from contextlib import AbstractAsyncContextManager
|
|
3
|
+
from typing import AsyncIterator, Optional
|
|
4
|
+
|
|
5
|
+
from flatfs import _export, _utils, _compat
|
|
6
|
+
|
|
7
|
+
from .interface import SupportsAsyncReadChunks, SupportsAsyncWriteChunks
|
|
8
|
+
|
|
9
|
+
__all__ = export = _export.Export() # type: ignore
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@export
|
|
13
|
+
async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: int = 65535) -> bytes:
|
|
14
|
+
"""Async variant of the :func:`read_bytes` helper."""
|
|
15
|
+
out = b""
|
|
16
|
+
async for chunk in fs.read_chunks(path, chunk_size):
|
|
17
|
+
out += chunk
|
|
18
|
+
return out
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@export
|
|
22
|
+
async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
23
|
+
"""Async variant of the :func:`write_bytes` helper."""
|
|
24
|
+
total_bytes = 0
|
|
25
|
+
async for byte_count in fs.write_chunks(path, _utils.async_split_into_chunks(data, chunk_size)):
|
|
26
|
+
total_bytes += byte_count
|
|
27
|
+
return total_bytes
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@export
|
|
31
|
+
async def async_read_text(
|
|
32
|
+
fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
33
|
+
) -> str:
|
|
34
|
+
"""Async variant of the :func:`read_text` helper."""
|
|
35
|
+
return (await async_read_bytes(fs, path, chunk_size)).decode(encoding)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@export
|
|
39
|
+
async def async_write_text(
|
|
40
|
+
fs: SupportsAsyncWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
41
|
+
):
|
|
42
|
+
"""Async variant of the :func:`write_text` helper."""
|
|
43
|
+
await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@export
|
|
47
|
+
class AsyncBufferedWriter(AbstractAsyncContextManager):
|
|
48
|
+
"""Async variant of the :class:`BufferedWriter` class."""
|
|
49
|
+
|
|
50
|
+
def __init__(self, fs: SupportsAsyncWriteChunks, path: str, buffer_size: int = 65535):
|
|
51
|
+
self.__chunk_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
52
|
+
self.__buffer_size = buffer_size
|
|
53
|
+
self.__buffer = b""
|
|
54
|
+
self.__writer = fs.write_chunks(path, _utils.async_generate_chunks_from_queue(self.__chunk_queue))
|
|
55
|
+
|
|
56
|
+
async def __aenter__(self) -> "AsyncBufferedWriter":
|
|
57
|
+
return self
|
|
58
|
+
|
|
59
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
60
|
+
return await self.close()
|
|
61
|
+
|
|
62
|
+
async def __write_chunk(self, chunk: bytes):
|
|
63
|
+
self.__chunk_queue.put_nowait(chunk)
|
|
64
|
+
await _compat.anext(self.__writer)
|
|
65
|
+
|
|
66
|
+
async def write(self, data: bytes) -> int:
|
|
67
|
+
self.__buffer += data
|
|
68
|
+
while len(self.__buffer) > self.__buffer_size:
|
|
69
|
+
chunk = self.__buffer[: self.__buffer_size]
|
|
70
|
+
await self.__write_chunk(chunk)
|
|
71
|
+
self.__buffer = self.__buffer[self.__buffer_size :]
|
|
72
|
+
return len(data)
|
|
73
|
+
|
|
74
|
+
async def flush(self):
|
|
75
|
+
if not self.__buffer:
|
|
76
|
+
return # Nothing to do
|
|
77
|
+
await self.__write_chunk(self.__buffer)
|
|
78
|
+
self.__buffer = b""
|
|
79
|
+
|
|
80
|
+
async def close(self):
|
|
81
|
+
await self.flush()
|
|
82
|
+
await self.__writer.aclose()
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@export
|
|
86
|
+
class AsyncBufferedReader(AbstractAsyncContextManager):
|
|
87
|
+
"""Async variant of the :class:`BufferedReader` class."""
|
|
88
|
+
|
|
89
|
+
def __init__(self, fs: SupportsAsyncReadChunks, path: str, buffer_size: int = 65535):
|
|
90
|
+
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
91
|
+
self.__buffer = b""
|
|
92
|
+
|
|
93
|
+
async def __aenter__(self) -> "AsyncBufferedReader":
|
|
94
|
+
return self
|
|
95
|
+
|
|
96
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
97
|
+
return await self.close()
|
|
98
|
+
|
|
99
|
+
async def close(self):
|
|
100
|
+
self.__buffer = b""
|
|
101
|
+
await self.__reader.aclose()
|
|
102
|
+
|
|
103
|
+
async def read(self, count: int) -> bytes:
|
|
104
|
+
if not self.__buffer:
|
|
105
|
+
self.__buffer = await _compat.anext(self.__reader, b"")
|
|
106
|
+
data = self.__buffer[:count]
|
|
107
|
+
self.__buffer = self.__buffer[count:]
|
|
108
|
+
return data
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@export
|
|
112
|
+
class AsyncBufferedTextReader(AbstractAsyncContextManager):
|
|
113
|
+
"""Async variant of the :class:`BufferedTextReader` class."""
|
|
114
|
+
|
|
115
|
+
def __init__(self, fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", buffer_size: int = 65535):
|
|
116
|
+
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
117
|
+
self.__encoding = encoding
|
|
118
|
+
self.__buffer = ""
|
|
119
|
+
|
|
120
|
+
async def __read_next_chunk(self) -> str:
|
|
121
|
+
return (await _compat.anext(self.__reader, b"")).decode(self.__encoding)
|
|
122
|
+
|
|
123
|
+
async def __aenter__(self) -> "AsyncBufferedTextReader":
|
|
124
|
+
return self
|
|
125
|
+
|
|
126
|
+
async def __aexit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
127
|
+
return await self.close()
|
|
128
|
+
|
|
129
|
+
async def __aiter__(self) -> AsyncIterator[str]:
|
|
130
|
+
while next_chunk := await self.__read_next_chunk():
|
|
131
|
+
self.__buffer += next_chunk
|
|
132
|
+
lines = self.__buffer.splitlines()
|
|
133
|
+
if len(lines) > 1:
|
|
134
|
+
for line in lines[:-1]:
|
|
135
|
+
yield line
|
|
136
|
+
self.__buffer = lines[-1]
|
|
137
|
+
yield self.__buffer
|
|
138
|
+
|
|
139
|
+
async def read(self, count: int) -> str:
|
|
140
|
+
if not self.__buffer:
|
|
141
|
+
self.__buffer = await self.__read_next_chunk()
|
|
142
|
+
out = self.__buffer[:count]
|
|
143
|
+
self.__buffer = self.__buffer[count:]
|
|
144
|
+
return out
|
|
145
|
+
|
|
146
|
+
async def close(self):
|
|
147
|
+
self.__buffer = ""
|
|
148
|
+
await self.__reader.aclose()
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
from typing import AsyncGenerator, AsyncIterator, Protocol
|
|
2
|
+
|
|
3
|
+
from flatfs import _export
|
|
4
|
+
from flatfs.interface import Stat
|
|
5
|
+
|
|
6
|
+
__all__ = export = _export.Export() # type: ignore
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@export
|
|
10
|
+
class SupportsAsyncReadChunks(Protocol):
|
|
11
|
+
"""Async variant of the :class:`SupportsReadChunks` protocol."""
|
|
12
|
+
|
|
13
|
+
def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
|
|
14
|
+
"""Return a generator for reading file in chunks of given maximum size.
|
|
15
|
+
|
|
16
|
+
This is the async version of :meth:`SupportsReadChunks.read_chunks`
|
|
17
|
+
method.
|
|
18
|
+
|
|
19
|
+
:param path:
|
|
20
|
+
The path to a file to read.
|
|
21
|
+
|
|
22
|
+
:param chunk_size:
|
|
23
|
+
The maximum chunk size, in bytes.
|
|
24
|
+
"""
|
|
25
|
+
...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@export
|
|
29
|
+
class SupportsAsyncWriteChunks(Protocol):
|
|
30
|
+
"""Async variant of the :class:`SupportsWriteChunks` protocol."""
|
|
31
|
+
|
|
32
|
+
def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
|
|
33
|
+
"""Generator for creating or overwriting files.
|
|
34
|
+
|
|
35
|
+
This is the async version of :meth:`SupportsWriteChunks.write_chunks`
|
|
36
|
+
method.
|
|
37
|
+
|
|
38
|
+
:param path:
|
|
39
|
+
The path to a file to create or replace.
|
|
40
|
+
|
|
41
|
+
:param chunk_gen:
|
|
42
|
+
Async generator yielding file chunks.
|
|
43
|
+
"""
|
|
44
|
+
...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@export
|
|
48
|
+
class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
|
|
49
|
+
"""Async variant of the :class:`FlatFsReader` protocol."""
|
|
50
|
+
|
|
51
|
+
def scan(self) -> AsyncIterator[str]:
|
|
52
|
+
"""Return iterator that yields normalized paths to existing files.
|
|
53
|
+
|
|
54
|
+
This is the async version of :meth:`FlatFsReader.scan` method.
|
|
55
|
+
"""
|
|
56
|
+
...
|
|
57
|
+
|
|
58
|
+
async def stat(self, path: str) -> Stat:
|
|
59
|
+
"""Get stats of a file at given path.
|
|
60
|
+
|
|
61
|
+
This is the async version of :meth:`FlatFsReader.stat` method.
|
|
62
|
+
|
|
63
|
+
:param path:
|
|
64
|
+
The path to a file.
|
|
65
|
+
"""
|
|
66
|
+
...
|
|
67
|
+
|
|
68
|
+
async def exists(self, path: str) -> bool:
|
|
69
|
+
"""Check if file at given path exists.
|
|
70
|
+
|
|
71
|
+
This is the async version of :meth:`FlatFsReader.exists` method.
|
|
72
|
+
|
|
73
|
+
:param path:
|
|
74
|
+
The path to check.
|
|
75
|
+
"""
|
|
76
|
+
...
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@export
|
|
80
|
+
class AsyncFlatFsReaderWriter(AsyncFlatFsReader, SupportsAsyncWriteChunks, Protocol):
|
|
81
|
+
"""Async variant of the :class:`FlatFsReaderWriter` protocol."""
|
|
82
|
+
|
|
83
|
+
async def remove(self, path: str):
|
|
84
|
+
"""Remove existing file.
|
|
85
|
+
|
|
86
|
+
This is the async version of :meth:`FlatFsReaderWriter.remove` method.
|
|
87
|
+
|
|
88
|
+
:param path:
|
|
89
|
+
The path to a file to remove.
|
|
90
|
+
"""
|
|
91
|
+
...
|
|
@@ -1,13 +1,21 @@
|
|
|
1
1
|
"""The all-in-one import helper for FlatFS public API."""
|
|
2
2
|
|
|
3
|
+
from .aio.api import __all__ as _aio_all
|
|
3
4
|
from .backends import __all__ as _backends_all
|
|
4
5
|
from .exc import __all__ as _exc_all
|
|
5
6
|
from .helpers import __all__ as _helpers_all
|
|
6
7
|
from .interface import __all__ as _interface_all
|
|
7
8
|
|
|
9
|
+
from .aio.api import *
|
|
8
10
|
from .backends import *
|
|
9
11
|
from .exc import *
|
|
10
12
|
from .helpers import *
|
|
11
13
|
from .interface import *
|
|
12
14
|
|
|
13
|
-
__all__ =
|
|
15
|
+
__all__ = (
|
|
16
|
+
*_aio_all,
|
|
17
|
+
*_backends_all,
|
|
18
|
+
*_exc_all,
|
|
19
|
+
*_helpers_all,
|
|
20
|
+
*_interface_all,
|
|
21
|
+
) # type: ignore
|
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import asyncio
|
|
2
1
|
import dataclasses
|
|
3
2
|
import datetime
|
|
4
3
|
import fnmatch
|
|
5
4
|
import os
|
|
6
5
|
import pathlib
|
|
7
|
-
from
|
|
8
|
-
from typing import AsyncGenerator, AsyncIterator, Generator, Iterator, Optional
|
|
6
|
+
from typing import Generator, Iterator, Optional
|
|
9
7
|
|
|
10
8
|
from flatfs.exc import PathAccessError, PathNotFoundError
|
|
11
|
-
from flatfs.interface import
|
|
9
|
+
from flatfs.interface import Stat
|
|
12
10
|
|
|
13
11
|
from . import _utils, _export
|
|
14
12
|
|
|
@@ -181,85 +179,3 @@ class InMemoryFlatFs:
|
|
|
181
179
|
if key not in self.__storage:
|
|
182
180
|
raise PathNotFoundError(path)
|
|
183
181
|
del self.__storage[key]
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@export
|
|
187
|
-
class AsyncFlatFsAdapter:
|
|
188
|
-
"""Adapter that wraps given :class:`flatfs.interface.FlatFsReaderWriter`
|
|
189
|
-
with async interface.
|
|
190
|
-
|
|
191
|
-
You can use this to adapt e.g. :class:`LocalFlatFs` or
|
|
192
|
-
:class:`InMemoryFlatFs` objects with async interface allowing for
|
|
193
|
-
convenient use with async code. The blocking calls are dispatched to
|
|
194
|
-
background worker which communicates with the caller using queues.
|
|
195
|
-
|
|
196
|
-
:param target:
|
|
197
|
-
The target FlatFS reader-writer to wrap.
|
|
198
|
-
"""
|
|
199
|
-
|
|
200
|
-
def __init__(self, target: FlatFsReaderWriter):
|
|
201
|
-
self.__target = target
|
|
202
|
-
|
|
203
|
-
async def scan(self) -> AsyncGenerator[str, None]:
|
|
204
|
-
|
|
205
|
-
def scanner():
|
|
206
|
-
for path in self.__target.scan():
|
|
207
|
-
loop.call_soon_threadsafe(queue.put_nowait, path)
|
|
208
|
-
loop.call_soon_threadsafe(queue.put_nowait, "")
|
|
209
|
-
|
|
210
|
-
loop = asyncio.get_running_loop()
|
|
211
|
-
asyncio.create_task(_utils.run_blocking(scanner))
|
|
212
|
-
queue: asyncio.Queue[str] = asyncio.Queue()
|
|
213
|
-
while True:
|
|
214
|
-
path = await queue.get()
|
|
215
|
-
if not path:
|
|
216
|
-
break
|
|
217
|
-
yield path
|
|
218
|
-
|
|
219
|
-
async def exists(self, path: str) -> bool:
|
|
220
|
-
return await _utils.run_blocking(self.__target.exists, path)
|
|
221
|
-
|
|
222
|
-
async def stat(self, path: str) -> Stat:
|
|
223
|
-
return await _utils.run_blocking(self.__target.stat, path)
|
|
224
|
-
|
|
225
|
-
async def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncIterator[bytes]:
|
|
226
|
-
|
|
227
|
-
def reader():
|
|
228
|
-
try:
|
|
229
|
-
for chunk in self.__target.read_chunks(path, chunk_size):
|
|
230
|
-
loop.call_soon_threadsafe(queue.put_nowait, chunk)
|
|
231
|
-
loop.call_soon_threadsafe(queue.put_nowait, b"")
|
|
232
|
-
except Exception as e:
|
|
233
|
-
loop.call_soon_threadsafe(queue.put_nowait, e)
|
|
234
|
-
|
|
235
|
-
loop = asyncio.get_running_loop()
|
|
236
|
-
queue: asyncio.Queue[bytes | Exception] = asyncio.Queue()
|
|
237
|
-
asyncio.create_task(_utils.run_blocking(reader))
|
|
238
|
-
while True:
|
|
239
|
-
chunk_or_exc = await queue.get()
|
|
240
|
-
if not chunk_or_exc:
|
|
241
|
-
break
|
|
242
|
-
if isinstance(chunk_or_exc, Exception):
|
|
243
|
-
raise chunk_or_exc
|
|
244
|
-
yield chunk_or_exc
|
|
245
|
-
|
|
246
|
-
async def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
|
|
247
|
-
|
|
248
|
-
def blocking_writer():
|
|
249
|
-
for write_count in self.__target.write_chunks(path, _utils.generate_chunks_from_queue(chunk_queue)):
|
|
250
|
-
loop.call_soon_threadsafe(ack_queue.put_nowait, write_count)
|
|
251
|
-
|
|
252
|
-
loop = asyncio.get_running_loop()
|
|
253
|
-
chunk_queue: Queue[bytes] = Queue()
|
|
254
|
-
ack_queue: asyncio.Queue[int] = asyncio.Queue()
|
|
255
|
-
task = asyncio.create_task(_utils.run_blocking(blocking_writer))
|
|
256
|
-
try:
|
|
257
|
-
async for chunk in chunk_gen:
|
|
258
|
-
chunk_queue.put(chunk)
|
|
259
|
-
yield await ack_queue.get()
|
|
260
|
-
finally:
|
|
261
|
-
chunk_queue.put(b"")
|
|
262
|
-
await task
|
|
263
|
-
|
|
264
|
-
async def remove(self, path: str):
|
|
265
|
-
await _utils.run_blocking(self.__target.remove, path)
|
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
import
|
|
2
|
-
from contextlib import AbstractAsyncContextManager, AbstractContextManager
|
|
1
|
+
from contextlib import AbstractContextManager
|
|
3
2
|
from queue import Queue
|
|
4
|
-
from
|
|
5
|
-
from typing import Iterator, AsyncIterator
|
|
3
|
+
from typing import Iterator, Optional
|
|
6
4
|
|
|
7
5
|
from . import _export, _utils
|
|
8
|
-
from .interface import
|
|
6
|
+
from .interface import SupportsReadChunks, SupportsWriteChunks
|
|
9
7
|
|
|
10
8
|
__all__ = export = _export.Export() # type: ignore
|
|
11
9
|
|
|
@@ -34,15 +32,6 @@ def read_bytes(fs: SupportsReadChunks, path: str, chunk_size: int = 65535) -> by
|
|
|
34
32
|
return out
|
|
35
33
|
|
|
36
34
|
|
|
37
|
-
@export
|
|
38
|
-
async def async_read_bytes(fs: SupportsAsyncReadChunks, path: str, chunk_size: int = 65535) -> bytes:
|
|
39
|
-
"""Async variant of the :func:`read_bytes` helper."""
|
|
40
|
-
out = b""
|
|
41
|
-
async for chunk in fs.read_chunks(path, chunk_size):
|
|
42
|
-
out += chunk
|
|
43
|
-
return out
|
|
44
|
-
|
|
45
|
-
|
|
46
35
|
@export
|
|
47
36
|
def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
48
37
|
"""Write given data to file at given path either creating the file, or
|
|
@@ -73,15 +62,6 @@ def write_bytes(fs: SupportsWriteChunks, path: str, data: bytes, chunk_size: int
|
|
|
73
62
|
return total_bytes
|
|
74
63
|
|
|
75
64
|
|
|
76
|
-
@export
|
|
77
|
-
async def async_write_bytes(fs: SupportsAsyncWriteChunks, path: str, data: bytes, chunk_size: int = 65535) -> int:
|
|
78
|
-
"""Async variant of the :func:`write_bytes` helper."""
|
|
79
|
-
total_bytes = 0
|
|
80
|
-
async for byte_count in fs.write_chunks(path, _utils.async_split_into_chunks(data, chunk_size)):
|
|
81
|
-
total_bytes += byte_count
|
|
82
|
-
return total_bytes
|
|
83
|
-
|
|
84
|
-
|
|
85
65
|
@export
|
|
86
66
|
def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535) -> str:
|
|
87
67
|
"""Read whole text file and return as string.
|
|
@@ -106,14 +86,6 @@ def read_text(fs: SupportsReadChunks, path: str, encoding: str = "utf-8", chunk_
|
|
|
106
86
|
return read_bytes(fs, path, chunk_size).decode(encoding)
|
|
107
87
|
|
|
108
88
|
|
|
109
|
-
@export
|
|
110
|
-
async def async_read_text(
|
|
111
|
-
fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
112
|
-
) -> str:
|
|
113
|
-
"""Async variant of the :func:`read_text` helper."""
|
|
114
|
-
return (await async_read_bytes(fs, path, chunk_size)).decode(encoding)
|
|
115
|
-
|
|
116
|
-
|
|
117
89
|
@export
|
|
118
90
|
def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535):
|
|
119
91
|
"""Create new text file or replace existing with given text data.
|
|
@@ -138,14 +110,6 @@ def write_text(fs: SupportsWriteChunks, path: str, data: str, encoding: str = "u
|
|
|
138
110
|
return write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
139
111
|
|
|
140
112
|
|
|
141
|
-
@export
|
|
142
|
-
async def async_write_text(
|
|
143
|
-
fs: SupportsAsyncWriteChunks, path: str, data: str, encoding: str = "utf-8", chunk_size: int = 65535
|
|
144
|
-
):
|
|
145
|
-
"""Async variant of the :func:`write_text` helper."""
|
|
146
|
-
await async_write_bytes(fs, path, data.encode(encoding), chunk_size)
|
|
147
|
-
|
|
148
|
-
|
|
149
113
|
@export
|
|
150
114
|
class BufferedWriter(AbstractContextManager):
|
|
151
115
|
"""Buffered writer for creating new or replacing existing files.
|
|
@@ -180,9 +144,7 @@ class BufferedWriter(AbstractContextManager):
|
|
|
180
144
|
def __enter__(self) -> "BufferedWriter":
|
|
181
145
|
return self
|
|
182
146
|
|
|
183
|
-
def __exit__(
|
|
184
|
-
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
185
|
-
) -> bool | None:
|
|
147
|
+
def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
186
148
|
return self.close()
|
|
187
149
|
|
|
188
150
|
def __write_chunk(self, chunk: bytes):
|
|
@@ -221,47 +183,6 @@ class BufferedWriter(AbstractContextManager):
|
|
|
221
183
|
return len(data)
|
|
222
184
|
|
|
223
185
|
|
|
224
|
-
@export
|
|
225
|
-
class AsyncBufferedWriter(AbstractAsyncContextManager):
|
|
226
|
-
"""Async variant of the :class:`BufferedWriter` class."""
|
|
227
|
-
|
|
228
|
-
def __init__(self, fs: SupportsAsyncWriteChunks, path: str, buffer_size: int = 65535):
|
|
229
|
-
self.__chunk_queue: asyncio.Queue[bytes] = asyncio.Queue()
|
|
230
|
-
self.__buffer_size = buffer_size
|
|
231
|
-
self.__buffer = b""
|
|
232
|
-
self.__writer = fs.write_chunks(path, _utils.async_generate_chunks_from_queue(self.__chunk_queue))
|
|
233
|
-
|
|
234
|
-
async def __aenter__(self) -> "AsyncBufferedWriter":
|
|
235
|
-
return self
|
|
236
|
-
|
|
237
|
-
async def __aexit__(
|
|
238
|
-
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
239
|
-
) -> bool | None:
|
|
240
|
-
return await self.close()
|
|
241
|
-
|
|
242
|
-
async def __write_chunk(self, chunk: bytes):
|
|
243
|
-
self.__chunk_queue.put_nowait(chunk)
|
|
244
|
-
await anext(self.__writer)
|
|
245
|
-
|
|
246
|
-
async def write(self, data: bytes) -> int:
|
|
247
|
-
self.__buffer += data
|
|
248
|
-
while len(self.__buffer) > self.__buffer_size:
|
|
249
|
-
chunk = self.__buffer[: self.__buffer_size]
|
|
250
|
-
await self.__write_chunk(chunk)
|
|
251
|
-
self.__buffer = self.__buffer[self.__buffer_size :]
|
|
252
|
-
return len(data)
|
|
253
|
-
|
|
254
|
-
async def flush(self):
|
|
255
|
-
if not self.__buffer:
|
|
256
|
-
return # Nothing to do
|
|
257
|
-
await self.__write_chunk(self.__buffer)
|
|
258
|
-
self.__buffer = b""
|
|
259
|
-
|
|
260
|
-
async def close(self):
|
|
261
|
-
await self.flush()
|
|
262
|
-
await self.__writer.aclose()
|
|
263
|
-
|
|
264
|
-
|
|
265
186
|
@export
|
|
266
187
|
class BufferedReader(AbstractContextManager):
|
|
267
188
|
"""Helper for buffered reading from underlying flat filesystem.
|
|
@@ -292,9 +213,7 @@ class BufferedReader(AbstractContextManager):
|
|
|
292
213
|
def __enter__(self) -> "BufferedReader":
|
|
293
214
|
return self
|
|
294
215
|
|
|
295
|
-
def __exit__(
|
|
296
|
-
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
297
|
-
) -> bool | None:
|
|
216
|
+
def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
298
217
|
return self.close()
|
|
299
218
|
|
|
300
219
|
def close(self):
|
|
@@ -309,34 +228,6 @@ class BufferedReader(AbstractContextManager):
|
|
|
309
228
|
return data
|
|
310
229
|
|
|
311
230
|
|
|
312
|
-
@export
|
|
313
|
-
class AsyncBufferedReader(AbstractAsyncContextManager):
|
|
314
|
-
"""Async variant of the :class:`BufferedReader` class."""
|
|
315
|
-
|
|
316
|
-
def __init__(self, fs: SupportsAsyncReadChunks, path: str, buffer_size: int = 65535):
|
|
317
|
-
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
318
|
-
self.__buffer = b""
|
|
319
|
-
|
|
320
|
-
async def __aenter__(self) -> "AsyncBufferedReader":
|
|
321
|
-
return self
|
|
322
|
-
|
|
323
|
-
async def __aexit__(
|
|
324
|
-
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
325
|
-
) -> bool | None:
|
|
326
|
-
return await self.close()
|
|
327
|
-
|
|
328
|
-
async def close(self):
|
|
329
|
-
self.__buffer = b""
|
|
330
|
-
await self.__reader.aclose()
|
|
331
|
-
|
|
332
|
-
async def read(self, count: int) -> bytes:
|
|
333
|
-
if not self.__buffer:
|
|
334
|
-
self.__buffer = await anext(self.__reader, b"")
|
|
335
|
-
data = self.__buffer[:count]
|
|
336
|
-
self.__buffer = self.__buffer[count:]
|
|
337
|
-
return data
|
|
338
|
-
|
|
339
|
-
|
|
340
231
|
@export
|
|
341
232
|
class BufferedTextReader(AbstractContextManager):
|
|
342
233
|
"""Equivalent of :class:`BufferedReader` for text files.
|
|
@@ -371,9 +262,7 @@ class BufferedTextReader(AbstractContextManager):
|
|
|
371
262
|
def __enter__(self) -> "BufferedTextReader":
|
|
372
263
|
return self
|
|
373
264
|
|
|
374
|
-
def __exit__(
|
|
375
|
-
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
376
|
-
) -> bool | None:
|
|
265
|
+
def __exit__(self, exc_type, exc_value, traceback) -> Optional[bool]:
|
|
377
266
|
return self.close()
|
|
378
267
|
|
|
379
268
|
def __iter__(self) -> Iterator[str]:
|
|
@@ -405,45 +294,3 @@ class BufferedTextReader(AbstractContextManager):
|
|
|
405
294
|
"""
|
|
406
295
|
self.__buffer = ""
|
|
407
296
|
self.__reader.close()
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
@export
|
|
411
|
-
class AsyncBufferedTextReader(AbstractAsyncContextManager):
|
|
412
|
-
"""Async variant of the :class:`BufferedTextReader` class."""
|
|
413
|
-
|
|
414
|
-
def __init__(self, fs: SupportsAsyncReadChunks, path: str, encoding: str = "utf-8", buffer_size: int = 65535):
|
|
415
|
-
self.__reader = fs.read_chunks(path, chunk_size=buffer_size)
|
|
416
|
-
self.__encoding = encoding
|
|
417
|
-
self.__buffer = ""
|
|
418
|
-
|
|
419
|
-
async def __read_next_chunk(self) -> str:
|
|
420
|
-
return (await anext(self.__reader, b"")).decode(self.__encoding)
|
|
421
|
-
|
|
422
|
-
async def __aenter__(self) -> "AsyncBufferedTextReader":
|
|
423
|
-
return self
|
|
424
|
-
|
|
425
|
-
async def __aexit__(
|
|
426
|
-
self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None
|
|
427
|
-
) -> bool | None:
|
|
428
|
-
return await self.close()
|
|
429
|
-
|
|
430
|
-
async def __aiter__(self) -> AsyncIterator[str]:
|
|
431
|
-
while next_chunk := await self.__read_next_chunk():
|
|
432
|
-
self.__buffer += next_chunk
|
|
433
|
-
lines = self.__buffer.splitlines()
|
|
434
|
-
if len(lines) > 1:
|
|
435
|
-
for line in lines[:-1]:
|
|
436
|
-
yield line
|
|
437
|
-
self.__buffer = lines[-1]
|
|
438
|
-
yield self.__buffer
|
|
439
|
-
|
|
440
|
-
async def read(self, count: int) -> str:
|
|
441
|
-
if not self.__buffer:
|
|
442
|
-
self.__buffer = await self.__read_next_chunk()
|
|
443
|
-
out = self.__buffer[:count]
|
|
444
|
-
self.__buffer = self.__buffer[count:]
|
|
445
|
-
return out
|
|
446
|
-
|
|
447
|
-
async def close(self):
|
|
448
|
-
self.__buffer = ""
|
|
449
|
-
await self.__reader.aclose()
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import dataclasses
|
|
2
2
|
import datetime
|
|
3
|
-
from typing import
|
|
3
|
+
from typing import Generator, Iterator, Protocol
|
|
4
4
|
|
|
5
5
|
from . import _export
|
|
6
6
|
|
|
@@ -111,88 +111,3 @@ class FlatFsReaderWriter(FlatFsReader, SupportsWriteChunks, Protocol):
|
|
|
111
111
|
The path to a file to remove.
|
|
112
112
|
"""
|
|
113
113
|
...
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
@export
|
|
117
|
-
class SupportsAsyncReadChunks(Protocol):
|
|
118
|
-
"""Async variant of the :class:`SupportsReadChunks` protocol."""
|
|
119
|
-
|
|
120
|
-
def read_chunks(self, path: str, chunk_size: int = 65535) -> AsyncGenerator[bytes, None]:
|
|
121
|
-
"""Return a generator for reading file in chunks of given maximum size.
|
|
122
|
-
|
|
123
|
-
This is the async version of :meth:`SupportsReadChunks.read_chunks`
|
|
124
|
-
method.
|
|
125
|
-
|
|
126
|
-
:param path:
|
|
127
|
-
The path to a file to read.
|
|
128
|
-
|
|
129
|
-
:param chunk_size:
|
|
130
|
-
The maximum chunk size, in bytes.
|
|
131
|
-
"""
|
|
132
|
-
...
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
@export
|
|
136
|
-
class SupportsAsyncWriteChunks(Protocol):
|
|
137
|
-
"""Async variant of the :class:`SupportsWriteChunks` protocol."""
|
|
138
|
-
|
|
139
|
-
def write_chunks(self, path: str, chunk_gen: AsyncGenerator[bytes, None]) -> AsyncGenerator[int, None]:
|
|
140
|
-
"""Generator for creating or overwriting files.
|
|
141
|
-
|
|
142
|
-
This is the async version of :meth:`SupportsWriteChunks.write_chunks`
|
|
143
|
-
method.
|
|
144
|
-
|
|
145
|
-
:param path:
|
|
146
|
-
The path to a file to create or replace.
|
|
147
|
-
|
|
148
|
-
:param chunk_gen:
|
|
149
|
-
Async generator yielding file chunks.
|
|
150
|
-
"""
|
|
151
|
-
...
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
@export
|
|
155
|
-
class AsyncFlatFsReader(SupportsAsyncReadChunks, Protocol):
|
|
156
|
-
"""Async variant of the :class:`FlatFsReader` protocol."""
|
|
157
|
-
|
|
158
|
-
def scan(self) -> AsyncIterator[str]:
|
|
159
|
-
"""Return iterator that yields normalized paths to existing files.
|
|
160
|
-
|
|
161
|
-
This is the async version of :meth:`FlatFsReader.scan` method.
|
|
162
|
-
"""
|
|
163
|
-
...
|
|
164
|
-
|
|
165
|
-
async def stat(self, path: str) -> Stat:
|
|
166
|
-
"""Get stats of a file at given path.
|
|
167
|
-
|
|
168
|
-
This is the async version of :meth:`FlatFsReader.stat` method.
|
|
169
|
-
|
|
170
|
-
:param path:
|
|
171
|
-
The path to a file.
|
|
172
|
-
"""
|
|
173
|
-
...
|
|
174
|
-
|
|
175
|
-
async def exists(self, path: str) -> bool:
|
|
176
|
-
"""Check if file at given path exists.
|
|
177
|
-
|
|
178
|
-
This is the async version of :meth:`FlatFsReader.exists` method.
|
|
179
|
-
|
|
180
|
-
:param path:
|
|
181
|
-
The path to check.
|
|
182
|
-
"""
|
|
183
|
-
...
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
@export
|
|
187
|
-
class AsyncFlatFsReaderWriter(AsyncFlatFsReader, SupportsAsyncWriteChunks, Protocol):
|
|
188
|
-
"""Async variant of the :class:`FlatFsReaderWriter` protocol."""
|
|
189
|
-
|
|
190
|
-
async def remove(self, path: str):
|
|
191
|
-
"""Remove existing file.
|
|
192
|
-
|
|
193
|
-
This is the async version of :meth:`FlatFsReaderWriter.remove` method.
|
|
194
|
-
|
|
195
|
-
:param path:
|
|
196
|
-
The path to a file to remove.
|
|
197
|
-
"""
|
|
198
|
-
...
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "flatfs"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.5.0"
|
|
4
4
|
description = "Minimal filesystem abstraction with pluggable backends, treating paths as keys and file contents as values."
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "Maciej Wiatrzyk", email = "maciej.wiatrzyk@gmail.com"}
|
|
@@ -15,7 +15,7 @@ classifiers = [
|
|
|
15
15
|
"Operating System :: OS Independent",
|
|
16
16
|
"Topic :: Software Development :: Libraries",
|
|
17
17
|
]
|
|
18
|
-
requires-python = ">=3.
|
|
18
|
+
requires-python = ">=3.9,<4"
|
|
19
19
|
dependencies = [
|
|
20
20
|
]
|
|
21
21
|
|
|
@@ -28,11 +28,11 @@ requires = ["poetry-core>=2.0.0,<3.0.0"]
|
|
|
28
28
|
build-backend = "poetry.core.masonry.api"
|
|
29
29
|
|
|
30
30
|
[tool.poetry.group.dev.dependencies]
|
|
31
|
-
pytest = "^
|
|
31
|
+
pytest = "^8.4.2"
|
|
32
32
|
invoke = "^3.0.3"
|
|
33
|
-
pytest-asyncio = "^1.
|
|
33
|
+
pytest-asyncio = "^1.2.0"
|
|
34
34
|
mockify = "^0.14.0"
|
|
35
35
|
pytest-cov = "^7.1.0"
|
|
36
36
|
ruff = "^0.15.12"
|
|
37
|
-
mypy = "^
|
|
37
|
+
mypy = "^1.19.1"
|
|
38
38
|
bumpify = "^0.5.1"
|
flatfs-0.3.0/flatfs/__init__.py
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = "0.3.0"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|