ZhengyangZhang commited on
Commit
a7ad2e3
·
verified ·
1 Parent(s): 3bac6fb

Add files using upload-large-folder tool

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. lib/python3.12/site-packages/fsspec/__init__.py +69 -0
  2. lib/python3.12/site-packages/fsspec/__pycache__/__init__.cpython-312.pyc +0 -0
  3. lib/python3.12/site-packages/fsspec/__pycache__/_version.cpython-312.pyc +0 -0
  4. lib/python3.12/site-packages/fsspec/__pycache__/archive.cpython-312.pyc +0 -0
  5. lib/python3.12/site-packages/fsspec/__pycache__/asyn.cpython-312.pyc +0 -0
  6. lib/python3.12/site-packages/fsspec/__pycache__/caching.cpython-312.pyc +0 -0
  7. lib/python3.12/site-packages/fsspec/__pycache__/callbacks.cpython-312.pyc +0 -0
  8. lib/python3.12/site-packages/fsspec/__pycache__/compression.cpython-312.pyc +0 -0
  9. lib/python3.12/site-packages/fsspec/__pycache__/config.cpython-312.pyc +0 -0
  10. lib/python3.12/site-packages/fsspec/__pycache__/conftest.cpython-312.pyc +0 -0
  11. lib/python3.12/site-packages/fsspec/__pycache__/core.cpython-312.pyc +0 -0
  12. lib/python3.12/site-packages/fsspec/__pycache__/dircache.cpython-312.pyc +0 -0
  13. lib/python3.12/site-packages/fsspec/__pycache__/exceptions.cpython-312.pyc +0 -0
  14. lib/python3.12/site-packages/fsspec/__pycache__/fuse.cpython-312.pyc +0 -0
  15. lib/python3.12/site-packages/fsspec/__pycache__/generic.cpython-312.pyc +0 -0
  16. lib/python3.12/site-packages/fsspec/__pycache__/gui.cpython-312.pyc +0 -0
  17. lib/python3.12/site-packages/fsspec/__pycache__/json.cpython-312.pyc +0 -0
  18. lib/python3.12/site-packages/fsspec/__pycache__/mapping.cpython-312.pyc +0 -0
  19. lib/python3.12/site-packages/fsspec/__pycache__/parquet.cpython-312.pyc +0 -0
  20. lib/python3.12/site-packages/fsspec/__pycache__/registry.cpython-312.pyc +0 -0
  21. lib/python3.12/site-packages/fsspec/__pycache__/spec.cpython-312.pyc +0 -0
  22. lib/python3.12/site-packages/fsspec/__pycache__/transaction.cpython-312.pyc +0 -0
  23. lib/python3.12/site-packages/fsspec/__pycache__/utils.cpython-312.pyc +0 -0
  24. lib/python3.12/site-packages/fsspec/_version.py +21 -0
  25. lib/python3.12/site-packages/fsspec/archive.py +75 -0
  26. lib/python3.12/site-packages/fsspec/asyn.py +1098 -0
  27. lib/python3.12/site-packages/fsspec/caching.py +966 -0
  28. lib/python3.12/site-packages/fsspec/callbacks.py +324 -0
  29. lib/python3.12/site-packages/fsspec/compression.py +175 -0
  30. lib/python3.12/site-packages/fsspec/config.py +131 -0
  31. lib/python3.12/site-packages/fsspec/conftest.py +55 -0
  32. lib/python3.12/site-packages/fsspec/core.py +743 -0
  33. lib/python3.12/site-packages/fsspec/dircache.py +98 -0
  34. lib/python3.12/site-packages/fsspec/exceptions.py +18 -0
  35. lib/python3.12/site-packages/fsspec/fuse.py +324 -0
  36. lib/python3.12/site-packages/fsspec/generic.py +411 -0
  37. lib/python3.12/site-packages/fsspec/gui.py +416 -0
  38. lib/python3.12/site-packages/fsspec/implementations/__init__.py +0 -0
  39. lib/python3.12/site-packages/fsspec/implementations/__pycache__/__init__.cpython-312.pyc +0 -0
  40. lib/python3.12/site-packages/fsspec/implementations/__pycache__/arrow.cpython-312.pyc +0 -0
  41. lib/python3.12/site-packages/fsspec/implementations/__pycache__/asyn_wrapper.cpython-312.pyc +0 -0
  42. lib/python3.12/site-packages/fsspec/implementations/__pycache__/cache_mapper.cpython-312.pyc +0 -0
  43. lib/python3.12/site-packages/fsspec/implementations/__pycache__/cache_metadata.cpython-312.pyc +0 -0
  44. lib/python3.12/site-packages/fsspec/implementations/__pycache__/cached.cpython-312.pyc +0 -0
  45. lib/python3.12/site-packages/fsspec/implementations/__pycache__/dask.cpython-312.pyc +0 -0
  46. lib/python3.12/site-packages/fsspec/implementations/__pycache__/data.cpython-312.pyc +0 -0
  47. lib/python3.12/site-packages/fsspec/implementations/__pycache__/dbfs.cpython-312.pyc +0 -0
  48. lib/python3.12/site-packages/fsspec/implementations/__pycache__/dirfs.cpython-312.pyc +0 -0
  49. lib/python3.12/site-packages/fsspec/implementations/__pycache__/ftp.cpython-312.pyc +0 -0
  50. lib/python3.12/site-packages/fsspec/implementations/__pycache__/git.cpython-312.pyc +0 -0
lib/python3.12/site-packages/fsspec/__init__.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from importlib.metadata import entry_points
2
+
3
+ from . import caching
4
+ from ._version import __version__ # noqa: F401
5
+ from .callbacks import Callback
6
+ from .compression import available_compressions
7
+ from .core import get_fs_token_paths, open, open_files, open_local, url_to_fs
8
+ from .exceptions import FSTimeoutError
9
+ from .mapping import FSMap, get_mapper
10
+ from .registry import (
11
+ available_protocols,
12
+ filesystem,
13
+ get_filesystem_class,
14
+ register_implementation,
15
+ registry,
16
+ )
17
+ from .spec import AbstractFileSystem
18
+
19
+ __all__ = [
20
+ "AbstractFileSystem",
21
+ "FSTimeoutError",
22
+ "FSMap",
23
+ "filesystem",
24
+ "register_implementation",
25
+ "get_filesystem_class",
26
+ "get_fs_token_paths",
27
+ "get_mapper",
28
+ "open",
29
+ "open_files",
30
+ "open_local",
31
+ "registry",
32
+ "caching",
33
+ "Callback",
34
+ "available_protocols",
35
+ "available_compressions",
36
+ "url_to_fs",
37
+ ]
38
+
39
+
40
+ def process_entries():
41
+ if entry_points is not None:
42
+ try:
43
+ eps = entry_points()
44
+ except TypeError:
45
+ pass # importlib-metadata < 0.8
46
+ else:
47
+ if hasattr(eps, "select"): # Python 3.10+ / importlib_metadata >= 3.9.0
48
+ specs = eps.select(group="fsspec.specs")
49
+ else:
50
+ specs = eps.get("fsspec.specs", [])
51
+ registered_names = {}
52
+ for spec in specs:
53
+ err_msg = f"Unable to load filesystem from {spec}"
54
+ name = spec.name
55
+ if name in registered_names:
56
+ continue
57
+ registered_names[name] = True
58
+ register_implementation(
59
+ name,
60
+ spec.value.replace(":", "."),
61
+ errtxt=err_msg,
62
+ # We take our implementations as the ones to overload with if
63
+ # for some reason we encounter some, may be the same, already
64
+ # registered
65
+ clobber=True,
66
+ )
67
+
68
+
69
+ process_entries()
lib/python3.12/site-packages/fsspec/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (1.83 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/_version.cpython-312.pyc ADDED
Binary file (649 Bytes). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/archive.cpython-312.pyc ADDED
Binary file (4.11 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/asyn.cpython-312.pyc ADDED
Binary file (45.8 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/caching.cpython-312.pyc ADDED
Binary file (38.5 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/callbacks.cpython-312.pyc ADDED
Binary file (12.9 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/compression.cpython-312.pyc ADDED
Binary file (7.24 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/config.cpython-312.pyc ADDED
Binary file (5.99 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/conftest.cpython-312.pyc ADDED
Binary file (3.13 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/core.cpython-312.pyc ADDED
Binary file (28.3 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/dircache.cpython-312.pyc ADDED
Binary file (4.53 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/exceptions.cpython-312.pyc ADDED
Binary file (841 Bytes). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/fuse.cpython-312.pyc ADDED
Binary file (15.7 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/generic.cpython-312.pyc ADDED
Binary file (19.6 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/gui.cpython-312.pyc ADDED
Binary file (21.6 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/json.cpython-312.pyc ADDED
Binary file (6.19 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/mapping.cpython-312.pyc ADDED
Binary file (12.3 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/parquet.cpython-312.pyc ADDED
Binary file (15.7 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/registry.cpython-312.pyc ADDED
Binary file (11.2 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/spec.cpython-312.pyc ADDED
Binary file (89.5 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/transaction.cpython-312.pyc ADDED
Binary file (4.59 kB). View file
 
lib/python3.12/site-packages/fsspec/__pycache__/utils.cpython-312.pyc ADDED
Binary file (28.3 kB). View file
 
lib/python3.12/site-packages/fsspec/_version.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # file generated by setuptools-scm
2
+ # don't change, don't track in version control
3
+
4
+ __all__ = ["__version__", "__version_tuple__", "version", "version_tuple"]
5
+
6
+ TYPE_CHECKING = False
7
+ if TYPE_CHECKING:
8
+ from typing import Tuple
9
+ from typing import Union
10
+
11
+ VERSION_TUPLE = Tuple[Union[int, str], ...]
12
+ else:
13
+ VERSION_TUPLE = object
14
+
15
+ version: str
16
+ __version__: str
17
+ __version_tuple__: VERSION_TUPLE
18
+ version_tuple: VERSION_TUPLE
19
+
20
+ __version__ = version = '2025.3.0'
21
+ __version_tuple__ = version_tuple = (2025, 3, 0)
lib/python3.12/site-packages/fsspec/archive.py ADDED
@@ -0,0 +1,75 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import operator
2
+
3
+ from fsspec import AbstractFileSystem
4
+ from fsspec.utils import tokenize
5
+
6
+
7
+ class AbstractArchiveFileSystem(AbstractFileSystem):
8
+ """
9
+ A generic superclass for implementing Archive-based filesystems.
10
+
11
+ Currently, it is shared amongst
12
+ :class:`~fsspec.implementations.zip.ZipFileSystem`,
13
+ :class:`~fsspec.implementations.libarchive.LibArchiveFileSystem` and
14
+ :class:`~fsspec.implementations.tar.TarFileSystem`.
15
+ """
16
+
17
+ def __str__(self):
18
+ return f"<Archive-like object {type(self).__name__} at {id(self)}>"
19
+
20
+ __repr__ = __str__
21
+
22
+ def ukey(self, path):
23
+ return tokenize(path, self.fo, self.protocol)
24
+
25
+ def _all_dirnames(self, paths):
26
+ """Returns *all* directory names for each path in paths, including intermediate
27
+ ones.
28
+
29
+ Parameters
30
+ ----------
31
+ paths: Iterable of path strings
32
+ """
33
+ if len(paths) == 0:
34
+ return set()
35
+
36
+ dirnames = {self._parent(path) for path in paths} - {self.root_marker}
37
+ return dirnames | self._all_dirnames(dirnames)
38
+
39
+ def info(self, path, **kwargs):
40
+ self._get_dirs()
41
+ path = self._strip_protocol(path)
42
+ if path in {"", "/"} and self.dir_cache:
43
+ return {"name": "", "type": "directory", "size": 0}
44
+ if path in self.dir_cache:
45
+ return self.dir_cache[path]
46
+ elif path + "/" in self.dir_cache:
47
+ return self.dir_cache[path + "/"]
48
+ else:
49
+ raise FileNotFoundError(path)
50
+
51
+ def ls(self, path, detail=True, **kwargs):
52
+ self._get_dirs()
53
+ paths = {}
54
+ for p, f in self.dir_cache.items():
55
+ p = p.rstrip("/")
56
+ if "/" in p:
57
+ root = p.rsplit("/", 1)[0]
58
+ else:
59
+ root = ""
60
+ if root == path.rstrip("/"):
61
+ paths[p] = f
62
+ elif all(
63
+ (a == b)
64
+ for a, b in zip(path.split("/"), [""] + p.strip("/").split("/"))
65
+ ):
66
+ # root directory entry
67
+ ppath = p.rstrip("/").split("/", 1)[0]
68
+ if ppath not in paths:
69
+ out = {"name": ppath, "size": 0, "type": "directory"}
70
+ paths[ppath] = out
71
+ if detail:
72
+ out = sorted(paths.values(), key=operator.itemgetter("name"))
73
+ return out
74
+ else:
75
+ return sorted(paths)
lib/python3.12/site-packages/fsspec/asyn.py ADDED
@@ -0,0 +1,1098 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import asyncio.events
3
+ import functools
4
+ import inspect
5
+ import io
6
+ import numbers
7
+ import os
8
+ import re
9
+ import threading
10
+ from contextlib import contextmanager
11
+ from glob import has_magic
12
+ from typing import TYPE_CHECKING, Iterable
13
+
14
+ from .callbacks import DEFAULT_CALLBACK
15
+ from .exceptions import FSTimeoutError
16
+ from .implementations.local import LocalFileSystem, make_path_posix, trailing_sep
17
+ from .spec import AbstractBufferedFile, AbstractFileSystem
18
+ from .utils import glob_translate, is_exception, other_paths
19
+
20
+ private = re.compile("_[^_]")
21
+ iothread = [None] # dedicated fsspec IO thread
22
+ loop = [None] # global event loop for any non-async instance
23
+ _lock = None # global lock placeholder
24
+ get_running_loop = asyncio.get_running_loop
25
+
26
+
27
+ def get_lock():
28
+ """Allocate or return a threading lock.
29
+
30
+ The lock is allocated on first use to allow setting one lock per forked process.
31
+ """
32
+ global _lock
33
+ if not _lock:
34
+ _lock = threading.Lock()
35
+ return _lock
36
+
37
+
38
+ def reset_lock():
39
+ """Reset the global lock.
40
+
41
+ This should be called only on the init of a forked process to reset the lock to
42
+ None, enabling the new forked process to get a new lock.
43
+ """
44
+ global _lock
45
+
46
+ iothread[0] = None
47
+ loop[0] = None
48
+ _lock = None
49
+
50
+
51
+ async def _runner(event, coro, result, timeout=None):
52
+ timeout = timeout if timeout else None # convert 0 or 0.0 to None
53
+ if timeout is not None:
54
+ coro = asyncio.wait_for(coro, timeout=timeout)
55
+ try:
56
+ result[0] = await coro
57
+ except Exception as ex:
58
+ result[0] = ex
59
+ finally:
60
+ event.set()
61
+
62
+
63
+ def sync(loop, func, *args, timeout=None, **kwargs):
64
+ """
65
+ Make loop run coroutine until it returns. Runs in other thread
66
+
67
+ Examples
68
+ --------
69
+ >>> fsspec.asyn.sync(fsspec.asyn.get_loop(), func, *args,
70
+ timeout=timeout, **kwargs)
71
+ """
72
+ timeout = timeout if timeout else None # convert 0 or 0.0 to None
73
+ # NB: if the loop is not running *yet*, it is OK to submit work
74
+ # and we will wait for it
75
+ if loop is None or loop.is_closed():
76
+ raise RuntimeError("Loop is not running")
77
+ try:
78
+ loop0 = asyncio.events.get_running_loop()
79
+ if loop0 is loop:
80
+ raise NotImplementedError("Calling sync() from within a running loop")
81
+ except NotImplementedError:
82
+ raise
83
+ except RuntimeError:
84
+ pass
85
+ coro = func(*args, **kwargs)
86
+ result = [None]
87
+ event = threading.Event()
88
+ asyncio.run_coroutine_threadsafe(_runner(event, coro, result, timeout), loop)
89
+ while True:
90
+ # this loops allows thread to get interrupted
91
+ if event.wait(1):
92
+ break
93
+ if timeout is not None:
94
+ timeout -= 1
95
+ if timeout < 0:
96
+ raise FSTimeoutError
97
+
98
+ return_result = result[0]
99
+ if isinstance(return_result, asyncio.TimeoutError):
100
+ # suppress asyncio.TimeoutError, raise FSTimeoutError
101
+ raise FSTimeoutError from return_result
102
+ elif isinstance(return_result, BaseException):
103
+ raise return_result
104
+ else:
105
+ return return_result
106
+
107
+
108
+ def sync_wrapper(func, obj=None):
109
+ """Given a function, make so can be called in blocking contexts
110
+
111
+ Leave obj=None if defining within a class. Pass the instance if attaching
112
+ as an attribute of the instance.
113
+ """
114
+
115
+ @functools.wraps(func)
116
+ def wrapper(*args, **kwargs):
117
+ self = obj or args[0]
118
+ return sync(self.loop, func, *args, **kwargs)
119
+
120
+ return wrapper
121
+
122
+
123
+ @contextmanager
124
+ def _selector_policy():
125
+ original_policy = asyncio.get_event_loop_policy()
126
+ try:
127
+ if os.name == "nt" and hasattr(asyncio, "WindowsSelectorEventLoopPolicy"):
128
+ asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
129
+
130
+ yield
131
+ finally:
132
+ asyncio.set_event_loop_policy(original_policy)
133
+
134
+
135
+ def get_loop():
136
+ """Create or return the default fsspec IO loop
137
+
138
+ The loop will be running on a separate thread.
139
+ """
140
+ if loop[0] is None:
141
+ with get_lock():
142
+ # repeat the check just in case the loop got filled between the
143
+ # previous two calls from another thread
144
+ if loop[0] is None:
145
+ with _selector_policy():
146
+ loop[0] = asyncio.new_event_loop()
147
+ th = threading.Thread(target=loop[0].run_forever, name="fsspecIO")
148
+ th.daemon = True
149
+ th.start()
150
+ iothread[0] = th
151
+ return loop[0]
152
+
153
+
154
+ if TYPE_CHECKING:
155
+ import resource
156
+
157
+ ResourceError = resource.error
158
+ else:
159
+ try:
160
+ import resource
161
+ except ImportError:
162
+ resource = None
163
+ ResourceError = OSError
164
+ else:
165
+ ResourceError = getattr(resource, "error", OSError)
166
+
167
+ _DEFAULT_BATCH_SIZE = 128
168
+ _NOFILES_DEFAULT_BATCH_SIZE = 1280
169
+
170
+
171
+ def _get_batch_size(nofiles=False):
172
+ from fsspec.config import conf
173
+
174
+ if nofiles:
175
+ if "nofiles_gather_batch_size" in conf:
176
+ return conf["nofiles_gather_batch_size"]
177
+ else:
178
+ if "gather_batch_size" in conf:
179
+ return conf["gather_batch_size"]
180
+ if nofiles:
181
+ return _NOFILES_DEFAULT_BATCH_SIZE
182
+ if resource is None:
183
+ return _DEFAULT_BATCH_SIZE
184
+
185
+ try:
186
+ soft_limit, _ = resource.getrlimit(resource.RLIMIT_NOFILE)
187
+ except (ImportError, ValueError, ResourceError):
188
+ return _DEFAULT_BATCH_SIZE
189
+
190
+ if soft_limit == resource.RLIM_INFINITY:
191
+ return -1
192
+ else:
193
+ return soft_limit // 8
194
+
195
+
196
+ def running_async() -> bool:
197
+ """Being executed by an event loop?"""
198
+ try:
199
+ asyncio.get_running_loop()
200
+ return True
201
+ except RuntimeError:
202
+ return False
203
+
204
+
205
+ async def _run_coros_in_chunks(
206
+ coros,
207
+ batch_size=None,
208
+ callback=DEFAULT_CALLBACK,
209
+ timeout=None,
210
+ return_exceptions=False,
211
+ nofiles=False,
212
+ ):
213
+ """Run the given coroutines in chunks.
214
+
215
+ Parameters
216
+ ----------
217
+ coros: list of coroutines to run
218
+ batch_size: int or None
219
+ Number of coroutines to submit/wait on simultaneously.
220
+ If -1, then it will not be any throttling. If
221
+ None, it will be inferred from _get_batch_size()
222
+ callback: fsspec.callbacks.Callback instance
223
+ Gets a relative_update when each coroutine completes
224
+ timeout: number or None
225
+ If given, each coroutine times out after this time. Note that, since
226
+ there are multiple batches, the total run time of this function will in
227
+ general be longer
228
+ return_exceptions: bool
229
+ Same meaning as in asyncio.gather
230
+ nofiles: bool
231
+ If inferring the batch_size, does this operation involve local files?
232
+ If yes, you normally expect smaller batches.
233
+ """
234
+
235
+ if batch_size is None:
236
+ batch_size = _get_batch_size(nofiles=nofiles)
237
+
238
+ if batch_size == -1:
239
+ batch_size = len(coros)
240
+
241
+ assert batch_size > 0
242
+
243
+ async def _run_coro(coro, i):
244
+ try:
245
+ return await asyncio.wait_for(coro, timeout=timeout), i
246
+ except Exception as e:
247
+ if not return_exceptions:
248
+ raise
249
+ return e, i
250
+ finally:
251
+ callback.relative_update(1)
252
+
253
+ i = 0
254
+ n = len(coros)
255
+ results = [None] * n
256
+ pending = set()
257
+
258
+ while pending or i < n:
259
+ while len(pending) < batch_size and i < n:
260
+ pending.add(asyncio.ensure_future(_run_coro(coros[i], i)))
261
+ i += 1
262
+
263
+ if not pending:
264
+ break
265
+
266
+ done, pending = await asyncio.wait(pending, return_when=asyncio.FIRST_COMPLETED)
267
+ while done:
268
+ result, k = await done.pop()
269
+ results[k] = result
270
+
271
+ return results
272
+
273
+
274
+ # these methods should be implemented as async by any async-able backend
275
+ async_methods = [
276
+ "_ls",
277
+ "_cat_file",
278
+ "_get_file",
279
+ "_put_file",
280
+ "_rm_file",
281
+ "_cp_file",
282
+ "_pipe_file",
283
+ "_expand_path",
284
+ "_info",
285
+ "_isfile",
286
+ "_isdir",
287
+ "_exists",
288
+ "_walk",
289
+ "_glob",
290
+ "_find",
291
+ "_du",
292
+ "_size",
293
+ "_mkdir",
294
+ "_makedirs",
295
+ ]
296
+
297
+
298
+ class AsyncFileSystem(AbstractFileSystem):
299
+ """Async file operations, default implementations
300
+
301
+ Passes bulk operations to asyncio.gather for concurrent operation.
302
+
303
+ Implementations that have concurrent batch operations and/or async methods
304
+ should inherit from this class instead of AbstractFileSystem. Docstrings are
305
+ copied from the un-underscored method in AbstractFileSystem, if not given.
306
+ """
307
+
308
+ # note that methods do not have docstring here; they will be copied
309
+ # for _* methods and inferred for overridden methods.
310
+
311
+ async_impl = True
312
+ mirror_sync_methods = True
313
+ disable_throttling = False
314
+
315
+ def __init__(self, *args, asynchronous=False, loop=None, batch_size=None, **kwargs):
316
+ self.asynchronous = asynchronous
317
+ self._pid = os.getpid()
318
+ if not asynchronous:
319
+ self._loop = loop or get_loop()
320
+ else:
321
+ self._loop = None
322
+ self.batch_size = batch_size
323
+ super().__init__(*args, **kwargs)
324
+
325
+ @property
326
+ def loop(self):
327
+ if self._pid != os.getpid():
328
+ raise RuntimeError("This class is not fork-safe")
329
+ return self._loop
330
+
331
+ async def _rm_file(self, path, **kwargs):
332
+ raise NotImplementedError
333
+
334
+ async def _rm(self, path, recursive=False, batch_size=None, **kwargs):
335
+ # TODO: implement on_error
336
+ batch_size = batch_size or self.batch_size
337
+ path = await self._expand_path(path, recursive=recursive)
338
+ return await _run_coros_in_chunks(
339
+ [self._rm_file(p, **kwargs) for p in reversed(path)],
340
+ batch_size=batch_size,
341
+ nofiles=True,
342
+ )
343
+
344
+ async def _cp_file(self, path1, path2, **kwargs):
345
+ raise NotImplementedError
346
+
347
+ async def _mv_file(self, path1, path2):
348
+ await self._cp_file(path1, path2)
349
+ await self._rm_file(path1)
350
+
351
+ async def _copy(
352
+ self,
353
+ path1,
354
+ path2,
355
+ recursive=False,
356
+ on_error=None,
357
+ maxdepth=None,
358
+ batch_size=None,
359
+ **kwargs,
360
+ ):
361
+ if on_error is None and recursive:
362
+ on_error = "ignore"
363
+ elif on_error is None:
364
+ on_error = "raise"
365
+
366
+ if isinstance(path1, list) and isinstance(path2, list):
367
+ # No need to expand paths when both source and destination
368
+ # are provided as lists
369
+ paths1 = path1
370
+ paths2 = path2
371
+ else:
372
+ source_is_str = isinstance(path1, str)
373
+ paths1 = await self._expand_path(
374
+ path1, maxdepth=maxdepth, recursive=recursive
375
+ )
376
+ if source_is_str and (not recursive or maxdepth is not None):
377
+ # Non-recursive glob does not copy directories
378
+ paths1 = [
379
+ p for p in paths1 if not (trailing_sep(p) or await self._isdir(p))
380
+ ]
381
+ if not paths1:
382
+ return
383
+
384
+ source_is_file = len(paths1) == 1
385
+ dest_is_dir = isinstance(path2, str) and (
386
+ trailing_sep(path2) or await self._isdir(path2)
387
+ )
388
+
389
+ exists = source_is_str and (
390
+ (has_magic(path1) and source_is_file)
391
+ or (not has_magic(path1) and dest_is_dir and not trailing_sep(path1))
392
+ )
393
+ paths2 = other_paths(
394
+ paths1,
395
+ path2,
396
+ exists=exists,
397
+ flatten=not source_is_str,
398
+ )
399
+
400
+ batch_size = batch_size or self.batch_size
401
+ coros = [self._cp_file(p1, p2, **kwargs) for p1, p2 in zip(paths1, paths2)]
402
+ result = await _run_coros_in_chunks(
403
+ coros, batch_size=batch_size, return_exceptions=True, nofiles=True
404
+ )
405
+
406
+ for ex in filter(is_exception, result):
407
+ if on_error == "ignore" and isinstance(ex, FileNotFoundError):
408
+ continue
409
+ raise ex
410
+
411
+ async def _pipe_file(self, path, value, mode="overwrite", **kwargs):
412
+ raise NotImplementedError
413
+
414
+ async def _pipe(self, path, value=None, batch_size=None, **kwargs):
415
+ if isinstance(path, str):
416
+ path = {path: value}
417
+ batch_size = batch_size or self.batch_size
418
+ return await _run_coros_in_chunks(
419
+ [self._pipe_file(k, v, **kwargs) for k, v in path.items()],
420
+ batch_size=batch_size,
421
+ nofiles=True,
422
+ )
423
+
424
+ async def _process_limits(self, url, start, end):
425
+ """Helper for "Range"-based _cat_file"""
426
+ size = None
427
+ suff = False
428
+ if start is not None and start < 0:
429
+ # if start is negative and end None, end is the "suffix length"
430
+ if end is None:
431
+ end = -start
432
+ start = ""
433
+ suff = True
434
+ else:
435
+ size = size or (await self._info(url))["size"]
436
+ start = size + start
437
+ elif start is None:
438
+ start = 0
439
+ if not suff:
440
+ if end is not None and end < 0:
441
+ if start is not None:
442
+ size = size or (await self._info(url))["size"]
443
+ end = size + end
444
+ elif end is None:
445
+ end = ""
446
+ if isinstance(end, numbers.Integral):
447
+ end -= 1 # bytes range is inclusive
448
+ return f"bytes={start}-{end}"
449
+
450
+ async def _cat_file(self, path, start=None, end=None, **kwargs):
451
+ raise NotImplementedError
452
+
453
+ async def _cat(
454
+ self, path, recursive=False, on_error="raise", batch_size=None, **kwargs
455
+ ):
456
+ paths = await self._expand_path(path, recursive=recursive)
457
+ coros = [self._cat_file(path, **kwargs) for path in paths]
458
+ batch_size = batch_size or self.batch_size
459
+ out = await _run_coros_in_chunks(
460
+ coros, batch_size=batch_size, nofiles=True, return_exceptions=True
461
+ )
462
+ if on_error == "raise":
463
+ ex = next(filter(is_exception, out), False)
464
+ if ex:
465
+ raise ex
466
+ if (
467
+ len(paths) > 1
468
+ or isinstance(path, list)
469
+ or paths[0] != self._strip_protocol(path)
470
+ ):
471
+ return {
472
+ k: v
473
+ for k, v in zip(paths, out)
474
+ if on_error != "omit" or not is_exception(v)
475
+ }
476
+ else:
477
+ return out[0]
478
+
479
+ async def _cat_ranges(
480
+ self,
481
+ paths,
482
+ starts,
483
+ ends,
484
+ max_gap=None,
485
+ batch_size=None,
486
+ on_error="return",
487
+ **kwargs,
488
+ ):
489
+ """Get the contents of byte ranges from one or more files
490
+
491
+ Parameters
492
+ ----------
493
+ paths: list
494
+ A list of of filepaths on this filesystems
495
+ starts, ends: int or list
496
+ Bytes limits of the read. If using a single int, the same value will be
497
+ used to read all the specified files.
498
+ """
499
+ # TODO: on_error
500
+ if max_gap is not None:
501
+ # use utils.merge_offset_ranges
502
+ raise NotImplementedError
503
+ if not isinstance(paths, list):
504
+ raise TypeError
505
+ if not isinstance(starts, Iterable):
506
+ starts = [starts] * len(paths)
507
+ if not isinstance(ends, Iterable):
508
+ ends = [ends] * len(paths)
509
+ if len(starts) != len(paths) or len(ends) != len(paths):
510
+ raise ValueError
511
+ coros = [
512
+ self._cat_file(p, start=s, end=e, **kwargs)
513
+ for p, s, e in zip(paths, starts, ends)
514
+ ]
515
+ batch_size = batch_size or self.batch_size
516
+ return await _run_coros_in_chunks(
517
+ coros, batch_size=batch_size, nofiles=True, return_exceptions=True
518
+ )
519
+
520
+ async def _put_file(self, lpath, rpath, mode="overwrite", **kwargs):
521
+ raise NotImplementedError
522
+
523
+ async def _put(
524
+ self,
525
+ lpath,
526
+ rpath,
527
+ recursive=False,
528
+ callback=DEFAULT_CALLBACK,
529
+ batch_size=None,
530
+ maxdepth=None,
531
+ **kwargs,
532
+ ):
533
+ """Copy file(s) from local.
534
+
535
+ Copies a specific file or tree of files (if recursive=True). If rpath
536
+ ends with a "/", it will be assumed to be a directory, and target files
537
+ will go within.
538
+
539
+ The put_file method will be called concurrently on a batch of files. The
540
+ batch_size option can configure the amount of futures that can be executed
541
+ at the same time. If it is -1, then all the files will be uploaded concurrently.
542
+ The default can be set for this instance by passing "batch_size" in the
543
+ constructor, or for all instances by setting the "gather_batch_size" key
544
+ in ``fsspec.config.conf``, falling back to 1/8th of the system limit .
545
+ """
546
+ if isinstance(lpath, list) and isinstance(rpath, list):
547
+ # No need to expand paths when both source and destination
548
+ # are provided as lists
549
+ rpaths = rpath
550
+ lpaths = lpath
551
+ else:
552
+ source_is_str = isinstance(lpath, str)
553
+ if source_is_str:
554
+ lpath = make_path_posix(lpath)
555
+ fs = LocalFileSystem()
556
+ lpaths = fs.expand_path(lpath, recursive=recursive, maxdepth=maxdepth)
557
+ if source_is_str and (not recursive or maxdepth is not None):
558
+ # Non-recursive glob does not copy directories
559
+ lpaths = [p for p in lpaths if not (trailing_sep(p) or fs.isdir(p))]
560
+ if not lpaths:
561
+ return
562
+
563
+ source_is_file = len(lpaths) == 1
564
+ dest_is_dir = isinstance(rpath, str) and (
565
+ trailing_sep(rpath) or await self._isdir(rpath)
566
+ )
567
+
568
+ rpath = self._strip_protocol(rpath)
569
+ exists = source_is_str and (
570
+ (has_magic(lpath) and source_is_file)
571
+ or (not has_magic(lpath) and dest_is_dir and not trailing_sep(lpath))
572
+ )
573
+ rpaths = other_paths(
574
+ lpaths,
575
+ rpath,
576
+ exists=exists,
577
+ flatten=not source_is_str,
578
+ )
579
+
580
+ is_dir = {l: os.path.isdir(l) for l in lpaths}
581
+ rdirs = [r for l, r in zip(lpaths, rpaths) if is_dir[l]]
582
+ file_pairs = [(l, r) for l, r in zip(lpaths, rpaths) if not is_dir[l]]
583
+
584
+ await asyncio.gather(*[self._makedirs(d, exist_ok=True) for d in rdirs])
585
+ batch_size = batch_size or self.batch_size
586
+
587
+ coros = []
588
+ callback.set_size(len(file_pairs))
589
+ for lfile, rfile in file_pairs:
590
+ put_file = callback.branch_coro(self._put_file)
591
+ coros.append(put_file(lfile, rfile, **kwargs))
592
+
593
+ return await _run_coros_in_chunks(
594
+ coros, batch_size=batch_size, callback=callback
595
+ )
596
+
597
+ async def _get_file(self, rpath, lpath, **kwargs):
598
+ raise NotImplementedError
599
+
600
+ async def _get(
601
+ self,
602
+ rpath,
603
+ lpath,
604
+ recursive=False,
605
+ callback=DEFAULT_CALLBACK,
606
+ maxdepth=None,
607
+ **kwargs,
608
+ ):
609
+ """Copy file(s) to local.
610
+
611
+ Copies a specific file or tree of files (if recursive=True). If lpath
612
+ ends with a "/", it will be assumed to be a directory, and target files
613
+ will go within. Can submit a list of paths, which may be glob-patterns
614
+ and will be expanded.
615
+
616
+ The get_file method will be called concurrently on a batch of files. The
617
+ batch_size option can configure the amount of futures that can be executed
618
+ at the same time. If it is -1, then all the files will be uploaded concurrently.
619
+ The default can be set for this instance by passing "batch_size" in the
620
+ constructor, or for all instances by setting the "gather_batch_size" key
621
+ in ``fsspec.config.conf``, falling back to 1/8th of the system limit .
622
+ """
623
+ if isinstance(lpath, list) and isinstance(rpath, list):
624
+ # No need to expand paths when both source and destination
625
+ # are provided as lists
626
+ rpaths = rpath
627
+ lpaths = lpath
628
+ else:
629
+ source_is_str = isinstance(rpath, str)
630
+ # First check for rpath trailing slash as _strip_protocol removes it.
631
+ source_not_trailing_sep = source_is_str and not trailing_sep(rpath)
632
+ rpath = self._strip_protocol(rpath)
633
+ rpaths = await self._expand_path(
634
+ rpath, recursive=recursive, maxdepth=maxdepth
635
+ )
636
+ if source_is_str and (not recursive or maxdepth is not None):
637
+ # Non-recursive glob does not copy directories
638
+ rpaths = [
639
+ p for p in rpaths if not (trailing_sep(p) or await self._isdir(p))
640
+ ]
641
+ if not rpaths:
642
+ return
643
+
644
+ lpath = make_path_posix(lpath)
645
+ source_is_file = len(rpaths) == 1
646
+ dest_is_dir = isinstance(lpath, str) and (
647
+ trailing_sep(lpath) or LocalFileSystem().isdir(lpath)
648
+ )
649
+
650
+ exists = source_is_str and (
651
+ (has_magic(rpath) and source_is_file)
652
+ or (not has_magic(rpath) and dest_is_dir and source_not_trailing_sep)
653
+ )
654
+ lpaths = other_paths(
655
+ rpaths,
656
+ lpath,
657
+ exists=exists,
658
+ flatten=not source_is_str,
659
+ )
660
+
661
+ [os.makedirs(os.path.dirname(lp), exist_ok=True) for lp in lpaths]
662
+ batch_size = kwargs.pop("batch_size", self.batch_size)
663
+
664
+ coros = []
665
+ callback.set_size(len(lpaths))
666
+ for lpath, rpath in zip(lpaths, rpaths):
667
+ get_file = callback.branch_coro(self._get_file)
668
+ coros.append(get_file(rpath, lpath, **kwargs))
669
+ return await _run_coros_in_chunks(
670
+ coros, batch_size=batch_size, callback=callback
671
+ )
672
+
673
+ async def _isfile(self, path):
674
+ try:
675
+ return (await self._info(path))["type"] == "file"
676
+ except: # noqa: E722
677
+ return False
678
+
679
+ async def _isdir(self, path):
680
+ try:
681
+ return (await self._info(path))["type"] == "directory"
682
+ except OSError:
683
+ return False
684
+
685
+ async def _size(self, path):
686
+ return (await self._info(path)).get("size", None)
687
+
688
+ async def _sizes(self, paths, batch_size=None):
689
+ batch_size = batch_size or self.batch_size
690
+ return await _run_coros_in_chunks(
691
+ [self._size(p) for p in paths], batch_size=batch_size
692
+ )
693
+
694
+ async def _exists(self, path, **kwargs):
695
+ try:
696
+ await self._info(path, **kwargs)
697
+ return True
698
+ except FileNotFoundError:
699
+ return False
700
+
701
+ async def _info(self, path, **kwargs):
702
+ raise NotImplementedError
703
+
704
+ async def _ls(self, path, detail=True, **kwargs):
705
+ raise NotImplementedError
706
+
707
+ async def _walk(self, path, maxdepth=None, on_error="omit", **kwargs):
708
+ if maxdepth is not None and maxdepth < 1:
709
+ raise ValueError("maxdepth must be at least 1")
710
+
711
+ path = self._strip_protocol(path)
712
+ full_dirs = {}
713
+ dirs = {}
714
+ files = {}
715
+
716
+ detail = kwargs.pop("detail", False)
717
+ try:
718
+ listing = await self._ls(path, detail=True, **kwargs)
719
+ except (FileNotFoundError, OSError) as e:
720
+ if on_error == "raise":
721
+ raise
722
+ elif callable(on_error):
723
+ on_error(e)
724
+ if detail:
725
+ yield path, {}, {}
726
+ else:
727
+ yield path, [], []
728
+ return
729
+
730
+ for info in listing:
731
+ # each info name must be at least [path]/part , but here
732
+ # we check also for names like [path]/part/
733
+ pathname = info["name"].rstrip("/")
734
+ name = pathname.rsplit("/", 1)[-1]
735
+ if info["type"] == "directory" and pathname != path:
736
+ # do not include "self" path
737
+ full_dirs[name] = pathname
738
+ dirs[name] = info
739
+ elif pathname == path:
740
+ # file-like with same name as give path
741
+ files[""] = info
742
+ else:
743
+ files[name] = info
744
+
745
+ if detail:
746
+ yield path, dirs, files
747
+ else:
748
+ yield path, list(dirs), list(files)
749
+
750
+ if maxdepth is not None:
751
+ maxdepth -= 1
752
+ if maxdepth < 1:
753
+ return
754
+
755
+ for d in dirs:
756
+ async for _ in self._walk(
757
+ full_dirs[d], maxdepth=maxdepth, detail=detail, **kwargs
758
+ ):
759
+ yield _
760
+
761
+ async def _glob(self, path, maxdepth=None, **kwargs):
762
+ if maxdepth is not None and maxdepth < 1:
763
+ raise ValueError("maxdepth must be at least 1")
764
+
765
+ import re
766
+
767
+ seps = (os.path.sep, os.path.altsep) if os.path.altsep else (os.path.sep,)
768
+ ends_with_sep = path.endswith(seps) # _strip_protocol strips trailing slash
769
+ path = self._strip_protocol(path)
770
+ append_slash_to_dirname = ends_with_sep or path.endswith(
771
+ tuple(sep + "**" for sep in seps)
772
+ )
773
+ idx_star = path.find("*") if path.find("*") >= 0 else len(path)
774
+ idx_qmark = path.find("?") if path.find("?") >= 0 else len(path)
775
+ idx_brace = path.find("[") if path.find("[") >= 0 else len(path)
776
+
777
+ min_idx = min(idx_star, idx_qmark, idx_brace)
778
+
779
+ detail = kwargs.pop("detail", False)
780
+
781
+ if not has_magic(path):
782
+ if await self._exists(path, **kwargs):
783
+ if not detail:
784
+ return [path]
785
+ else:
786
+ return {path: await self._info(path, **kwargs)}
787
+ else:
788
+ if not detail:
789
+ return [] # glob of non-existent returns empty
790
+ else:
791
+ return {}
792
+ elif "/" in path[:min_idx]:
793
+ min_idx = path[:min_idx].rindex("/")
794
+ root = path[: min_idx + 1]
795
+ depth = path[min_idx + 1 :].count("/") + 1
796
+ else:
797
+ root = ""
798
+ depth = path[min_idx + 1 :].count("/") + 1
799
+
800
+ if "**" in path:
801
+ if maxdepth is not None:
802
+ idx_double_stars = path.find("**")
803
+ depth_double_stars = path[idx_double_stars:].count("/") + 1
804
+ depth = depth - depth_double_stars + maxdepth
805
+ else:
806
+ depth = None
807
+
808
+ allpaths = await self._find(
809
+ root, maxdepth=depth, withdirs=True, detail=True, **kwargs
810
+ )
811
+
812
+ pattern = glob_translate(path + ("/" if ends_with_sep else ""))
813
+ pattern = re.compile(pattern)
814
+
815
+ out = {
816
+ p: info
817
+ for p, info in sorted(allpaths.items())
818
+ if pattern.match(
819
+ p + "/"
820
+ if append_slash_to_dirname and info["type"] == "directory"
821
+ else p
822
+ )
823
+ }
824
+
825
+ if detail:
826
+ return out
827
+ else:
828
+ return list(out)
829
+
830
+ async def _du(self, path, total=True, maxdepth=None, **kwargs):
831
+ sizes = {}
832
+ # async for?
833
+ for f in await self._find(path, maxdepth=maxdepth, **kwargs):
834
+ info = await self._info(f)
835
+ sizes[info["name"]] = info["size"]
836
+ if total:
837
+ return sum(sizes.values())
838
+ else:
839
+ return sizes
840
+
841
+ async def _find(self, path, maxdepth=None, withdirs=False, **kwargs):
842
+ path = self._strip_protocol(path)
843
+ out = {}
844
+ detail = kwargs.pop("detail", False)
845
+
846
+ # Add the root directory if withdirs is requested
847
+ # This is needed for posix glob compliance
848
+ if withdirs and path != "" and await self._isdir(path):
849
+ out[path] = await self._info(path)
850
+
851
+ # async for?
852
+ async for _, dirs, files in self._walk(path, maxdepth, detail=True, **kwargs):
853
+ if withdirs:
854
+ files.update(dirs)
855
+ out.update({info["name"]: info for name, info in files.items()})
856
+ if not out and (await self._isfile(path)):
857
+ # walk works on directories, but find should also return [path]
858
+ # when path happens to be a file
859
+ out[path] = {}
860
+ names = sorted(out)
861
+ if not detail:
862
+ return names
863
+ else:
864
+ return {name: out[name] for name in names}
865
+
866
+ async def _expand_path(self, path, recursive=False, maxdepth=None):
867
+ if maxdepth is not None and maxdepth < 1:
868
+ raise ValueError("maxdepth must be at least 1")
869
+
870
+ if isinstance(path, str):
871
+ out = await self._expand_path([path], recursive, maxdepth)
872
+ else:
873
+ out = set()
874
+ path = [self._strip_protocol(p) for p in path]
875
+ for p in path: # can gather here
876
+ if has_magic(p):
877
+ bit = set(await self._glob(p, maxdepth=maxdepth))
878
+ out |= bit
879
+ if recursive:
880
+ # glob call above expanded one depth so if maxdepth is defined
881
+ # then decrement it in expand_path call below. If it is zero
882
+ # after decrementing then avoid expand_path call.
883
+ if maxdepth is not None and maxdepth <= 1:
884
+ continue
885
+ out |= set(
886
+ await self._expand_path(
887
+ list(bit),
888
+ recursive=recursive,
889
+ maxdepth=maxdepth - 1 if maxdepth is not None else None,
890
+ )
891
+ )
892
+ continue
893
+ elif recursive:
894
+ rec = set(await self._find(p, maxdepth=maxdepth, withdirs=True))
895
+ out |= rec
896
+ if p not in out and (recursive is False or (await self._exists(p))):
897
+ # should only check once, for the root
898
+ out.add(p)
899
+ if not out:
900
+ raise FileNotFoundError(path)
901
+ return sorted(out)
902
+
903
+ async def _mkdir(self, path, create_parents=True, **kwargs):
904
+ pass # not necessary to implement, may not have directories
905
+
906
+ async def _makedirs(self, path, exist_ok=False):
907
+ pass # not necessary to implement, may not have directories
908
+
909
+ async def open_async(self, path, mode="rb", **kwargs):
910
+ if "b" not in mode or kwargs.get("compression"):
911
+ raise ValueError
912
+ raise NotImplementedError
913
+
914
+
915
+ def mirror_sync_methods(obj):
916
+ """Populate sync and async methods for obj
917
+
918
+ For each method will create a sync version if the name refers to an async method
919
+ (coroutine) and there is no override in the child class; will create an async
920
+ method for the corresponding sync method if there is no implementation.
921
+
922
+ Uses the methods specified in
923
+ - async_methods: the set that an implementation is expected to provide
924
+ - default_async_methods: that can be derived from their sync version in
925
+ AbstractFileSystem
926
+ - AsyncFileSystem: async-specific default coroutines
927
+ """
928
+ from fsspec import AbstractFileSystem
929
+
930
+ for method in async_methods + dir(AsyncFileSystem):
931
+ if not method.startswith("_"):
932
+ continue
933
+ smethod = method[1:]
934
+ if private.match(method):
935
+ isco = inspect.iscoroutinefunction(getattr(obj, method, None))
936
+ unsync = getattr(getattr(obj, smethod, False), "__func__", None)
937
+ is_default = unsync is getattr(AbstractFileSystem, smethod, "")
938
+ if isco and is_default:
939
+ mth = sync_wrapper(getattr(obj, method), obj=obj)
940
+ setattr(obj, smethod, mth)
941
+ if not mth.__doc__:
942
+ mth.__doc__ = getattr(
943
+ getattr(AbstractFileSystem, smethod, None), "__doc__", ""
944
+ )
945
+
946
+
947
+ class FSSpecCoroutineCancel(Exception):
948
+ pass
949
+
950
+
951
+ def _dump_running_tasks(
952
+ printout=True, cancel=True, exc=FSSpecCoroutineCancel, with_task=False
953
+ ):
954
+ import traceback
955
+
956
+ tasks = [t for t in asyncio.tasks.all_tasks(loop[0]) if not t.done()]
957
+ if printout:
958
+ [task.print_stack() for task in tasks]
959
+ out = [
960
+ {
961
+ "locals": task._coro.cr_frame.f_locals,
962
+ "file": task._coro.cr_frame.f_code.co_filename,
963
+ "firstline": task._coro.cr_frame.f_code.co_firstlineno,
964
+ "linelo": task._coro.cr_frame.f_lineno,
965
+ "stack": traceback.format_stack(task._coro.cr_frame),
966
+ "task": task if with_task else None,
967
+ }
968
+ for task in tasks
969
+ ]
970
+ if cancel:
971
+ for t in tasks:
972
+ cbs = t._callbacks
973
+ t.cancel()
974
+ asyncio.futures.Future.set_exception(t, exc)
975
+ asyncio.futures.Future.cancel(t)
976
+ [cb[0](t) for cb in cbs] # cancels any dependent concurrent.futures
977
+ try:
978
+ t._coro.throw(exc) # exits coro, unless explicitly handled
979
+ except exc:
980
+ pass
981
+ return out
982
+
983
+
984
+ class AbstractAsyncStreamedFile(AbstractBufferedFile):
985
+ # no read buffering, and always auto-commit
986
+ # TODO: readahead might still be useful here, but needs async version
987
+
988
+ async def read(self, length=-1):
989
+ """
990
+ Return data from cache, or fetch pieces as necessary
991
+
992
+ Parameters
993
+ ----------
994
+ length: int (-1)
995
+ Number of bytes to read; if <0, all remaining bytes.
996
+ """
997
+ length = -1 if length is None else int(length)
998
+ if self.mode != "rb":
999
+ raise ValueError("File not in read mode")
1000
+ if length < 0:
1001
+ length = self.size - self.loc
1002
+ if self.closed:
1003
+ raise ValueError("I/O operation on closed file.")
1004
+ if length == 0:
1005
+ # don't even bother calling fetch
1006
+ return b""
1007
+ out = await self._fetch_range(self.loc, self.loc + length)
1008
+ self.loc += len(out)
1009
+ return out
1010
+
1011
+ async def write(self, data):
1012
+ """
1013
+ Write data to buffer.
1014
+
1015
+ Buffer only sent on flush() or if buffer is greater than
1016
+ or equal to blocksize.
1017
+
1018
+ Parameters
1019
+ ----------
1020
+ data: bytes
1021
+ Set of bytes to be written.
1022
+ """
1023
+ if self.mode not in {"wb", "ab"}:
1024
+ raise ValueError("File not in write mode")
1025
+ if self.closed:
1026
+ raise ValueError("I/O operation on closed file.")
1027
+ if self.forced:
1028
+ raise ValueError("This file has been force-flushed, can only close")
1029
+ out = self.buffer.write(data)
1030
+ self.loc += out
1031
+ if self.buffer.tell() >= self.blocksize:
1032
+ await self.flush()
1033
+ return out
1034
+
1035
+ async def close(self):
1036
+ """Close file
1037
+
1038
+ Finalizes writes, discards cache
1039
+ """
1040
+ if getattr(self, "_unclosable", False):
1041
+ return
1042
+ if self.closed:
1043
+ return
1044
+ if self.mode == "rb":
1045
+ self.cache = None
1046
+ else:
1047
+ if not self.forced:
1048
+ await self.flush(force=True)
1049
+
1050
+ if self.fs is not None:
1051
+ self.fs.invalidate_cache(self.path)
1052
+ self.fs.invalidate_cache(self.fs._parent(self.path))
1053
+
1054
+ self.closed = True
1055
+
1056
+ async def flush(self, force=False):
1057
+ if self.closed:
1058
+ raise ValueError("Flush on closed file")
1059
+ if force and self.forced:
1060
+ raise ValueError("Force flush cannot be called more than once")
1061
+ if force:
1062
+ self.forced = True
1063
+
1064
+ if self.mode not in {"wb", "ab"}:
1065
+ # no-op to flush on read-mode
1066
+ return
1067
+
1068
+ if not force and self.buffer.tell() < self.blocksize:
1069
+ # Defer write on small block
1070
+ return
1071
+
1072
+ if self.offset is None:
1073
+ # Initialize a multipart upload
1074
+ self.offset = 0
1075
+ try:
1076
+ await self._initiate_upload()
1077
+ except:
1078
+ self.closed = True
1079
+ raise
1080
+
1081
+ if await self._upload_chunk(final=force) is not False:
1082
+ self.offset += self.buffer.seek(0, 2)
1083
+ self.buffer = io.BytesIO()
1084
+
1085
+ async def __aenter__(self):
1086
+ return self
1087
+
1088
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
1089
+ await self.close()
1090
+
1091
+ async def _fetch_range(self, start, end):
1092
+ raise NotImplementedError
1093
+
1094
+ async def _initiate_upload(self):
1095
+ pass
1096
+
1097
+ async def _upload_chunk(self, final=False):
1098
+ raise NotImplementedError
lib/python3.12/site-packages/fsspec/caching.py ADDED
@@ -0,0 +1,966 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import collections
4
+ import functools
5
+ import logging
6
+ import math
7
+ import os
8
+ import threading
9
+ import warnings
10
+ from concurrent.futures import Future, ThreadPoolExecutor
11
+ from itertools import groupby
12
+ from operator import itemgetter
13
+ from typing import (
14
+ TYPE_CHECKING,
15
+ Any,
16
+ Callable,
17
+ ClassVar,
18
+ Generic,
19
+ NamedTuple,
20
+ Optional,
21
+ OrderedDict,
22
+ TypeVar,
23
+ )
24
+
25
+ if TYPE_CHECKING:
26
+ import mmap
27
+
28
+ from typing_extensions import ParamSpec
29
+
30
+ P = ParamSpec("P")
31
+ else:
32
+ P = TypeVar("P")
33
+
34
+ T = TypeVar("T")
35
+
36
+
37
+ logger = logging.getLogger("fsspec")
38
+
39
+ Fetcher = Callable[[int, int], bytes] # Maps (start, end) to bytes
40
+
41
+
42
+ class BaseCache:
43
+ """Pass-though cache: doesn't keep anything, calls every time
44
+
45
+ Acts as base class for other cachers
46
+
47
+ Parameters
48
+ ----------
49
+ blocksize: int
50
+ How far to read ahead in numbers of bytes
51
+ fetcher: func
52
+ Function of the form f(start, end) which gets bytes from remote as
53
+ specified
54
+ size: int
55
+ How big this file is
56
+ """
57
+
58
+ name: ClassVar[str] = "none"
59
+
60
+ def __init__(self, blocksize: int, fetcher: Fetcher, size: int) -> None:
61
+ self.blocksize = blocksize
62
+ self.nblocks = 0
63
+ self.fetcher = fetcher
64
+ self.size = size
65
+ self.hit_count = 0
66
+ self.miss_count = 0
67
+ # the bytes that we actually requested
68
+ self.total_requested_bytes = 0
69
+
70
+ def _fetch(self, start: int | None, stop: int | None) -> bytes:
71
+ if start is None:
72
+ start = 0
73
+ if stop is None:
74
+ stop = self.size
75
+ if start >= self.size or start >= stop:
76
+ return b""
77
+ return self.fetcher(start, stop)
78
+
79
+ def _reset_stats(self) -> None:
80
+ """Reset hit and miss counts for a more ganular report e.g. by file."""
81
+ self.hit_count = 0
82
+ self.miss_count = 0
83
+ self.total_requested_bytes = 0
84
+
85
+ def _log_stats(self) -> str:
86
+ """Return a formatted string of the cache statistics."""
87
+ if self.hit_count == 0 and self.miss_count == 0:
88
+ # a cache that does nothing, this is for logs only
89
+ return ""
90
+ return f" , {self.name}: {self.hit_count} hits, {self.miss_count} misses, {self.total_requested_bytes} total requested bytes"
91
+
92
+ def __repr__(self) -> str:
93
+ # TODO: use rich for better formatting
94
+ return f"""
95
+ <{self.__class__.__name__}:
96
+ block size : {self.blocksize}
97
+ block count : {self.nblocks}
98
+ file size : {self.size}
99
+ cache hits : {self.hit_count}
100
+ cache misses: {self.miss_count}
101
+ total requested bytes: {self.total_requested_bytes}>
102
+ """
103
+
104
+
105
+ class MMapCache(BaseCache):
106
+ """memory-mapped sparse file cache
107
+
108
+ Opens temporary file, which is filled blocks-wise when data is requested.
109
+ Ensure there is enough disc space in the temporary location.
110
+
111
+ This cache method might only work on posix
112
+ """
113
+
114
+ name = "mmap"
115
+
116
+ def __init__(
117
+ self,
118
+ blocksize: int,
119
+ fetcher: Fetcher,
120
+ size: int,
121
+ location: str | None = None,
122
+ blocks: set[int] | None = None,
123
+ ) -> None:
124
+ super().__init__(blocksize, fetcher, size)
125
+ self.blocks = set() if blocks is None else blocks
126
+ self.location = location
127
+ self.cache = self._makefile()
128
+
129
+ def _makefile(self) -> mmap.mmap | bytearray:
130
+ import mmap
131
+ import tempfile
132
+
133
+ if self.size == 0:
134
+ return bytearray()
135
+
136
+ # posix version
137
+ if self.location is None or not os.path.exists(self.location):
138
+ if self.location is None:
139
+ fd = tempfile.TemporaryFile()
140
+ self.blocks = set()
141
+ else:
142
+ fd = open(self.location, "wb+")
143
+ fd.seek(self.size - 1)
144
+ fd.write(b"1")
145
+ fd.flush()
146
+ else:
147
+ fd = open(self.location, "r+b")
148
+
149
+ return mmap.mmap(fd.fileno(), self.size)
150
+
151
+ def _fetch(self, start: int | None, end: int | None) -> bytes:
152
+ logger.debug(f"MMap cache fetching {start}-{end}")
153
+ if start is None:
154
+ start = 0
155
+ if end is None:
156
+ end = self.size
157
+ if start >= self.size or start >= end:
158
+ return b""
159
+ start_block = start // self.blocksize
160
+ end_block = end // self.blocksize
161
+ block_range = range(start_block, end_block + 1)
162
+ # Determine which blocks need to be fetched. This sequence is sorted by construction.
163
+ need = (i for i in block_range if i not in self.blocks)
164
+ # Count the number of blocks already cached
165
+ self.hit_count += sum(1 for i in block_range if i in self.blocks)
166
+
167
+ # Consolidate needed blocks.
168
+ # Algorithm adapted from Python 2.x itertools documentation.
169
+ # We are grouping an enumerated sequence of blocks. By comparing when the difference
170
+ # between an ascending range (provided by enumerate) and the needed block numbers
171
+ # we can detect when the block number skips values. The key computes this difference.
172
+ # Whenever the difference changes, we know that we have previously cached block(s),
173
+ # and a new group is started. In other words, this algorithm neatly groups
174
+ # runs of consecutive block numbers so they can be fetched together.
175
+ for _, _blocks in groupby(enumerate(need), key=lambda x: x[0] - x[1]):
176
+ # Extract the blocks from the enumerated sequence
177
+ _blocks = tuple(map(itemgetter(1), _blocks))
178
+ # Compute start of first block
179
+ sstart = _blocks[0] * self.blocksize
180
+ # Compute the end of the last block. Last block may not be full size.
181
+ send = min(_blocks[-1] * self.blocksize + self.blocksize, self.size)
182
+
183
+ # Fetch bytes (could be multiple consecutive blocks)
184
+ self.total_requested_bytes += send - sstart
185
+ logger.debug(
186
+ f"MMap get blocks {_blocks[0]}-{_blocks[-1]} ({sstart}-{send})"
187
+ )
188
+ self.cache[sstart:send] = self.fetcher(sstart, send)
189
+
190
+ # Update set of cached blocks
191
+ self.blocks.update(_blocks)
192
+ # Update cache statistics with number of blocks we had to cache
193
+ self.miss_count += len(_blocks)
194
+
195
+ return self.cache[start:end]
196
+
197
+ def __getstate__(self) -> dict[str, Any]:
198
+ state = self.__dict__.copy()
199
+ # Remove the unpicklable entries.
200
+ del state["cache"]
201
+ return state
202
+
203
+ def __setstate__(self, state: dict[str, Any]) -> None:
204
+ # Restore instance attributes
205
+ self.__dict__.update(state)
206
+ self.cache = self._makefile()
207
+
208
+
209
+ class ReadAheadCache(BaseCache):
210
+ """Cache which reads only when we get beyond a block of data
211
+
212
+ This is a much simpler version of BytesCache, and does not attempt to
213
+ fill holes in the cache or keep fragments alive. It is best suited to
214
+ many small reads in a sequential order (e.g., reading lines from a file).
215
+ """
216
+
217
+ name = "readahead"
218
+
219
+ def __init__(self, blocksize: int, fetcher: Fetcher, size: int) -> None:
220
+ super().__init__(blocksize, fetcher, size)
221
+ self.cache = b""
222
+ self.start = 0
223
+ self.end = 0
224
+
225
+ def _fetch(self, start: int | None, end: int | None) -> bytes:
226
+ if start is None:
227
+ start = 0
228
+ if end is None or end > self.size:
229
+ end = self.size
230
+ if start >= self.size or start >= end:
231
+ return b""
232
+ l = end - start
233
+ if start >= self.start and end <= self.end:
234
+ # cache hit
235
+ self.hit_count += 1
236
+ return self.cache[start - self.start : end - self.start]
237
+ elif self.start <= start < self.end:
238
+ # partial hit
239
+ self.miss_count += 1
240
+ part = self.cache[start - self.start :]
241
+ l -= len(part)
242
+ start = self.end
243
+ else:
244
+ # miss
245
+ self.miss_count += 1
246
+ part = b""
247
+ end = min(self.size, end + self.blocksize)
248
+ self.total_requested_bytes += end - start
249
+ self.cache = self.fetcher(start, end) # new block replaces old
250
+ self.start = start
251
+ self.end = self.start + len(self.cache)
252
+ return part + self.cache[:l]
253
+
254
+
255
+ class FirstChunkCache(BaseCache):
256
+ """Caches the first block of a file only
257
+
258
+ This may be useful for file types where the metadata is stored in the header,
259
+ but is randomly accessed.
260
+ """
261
+
262
+ name = "first"
263
+
264
+ def __init__(self, blocksize: int, fetcher: Fetcher, size: int) -> None:
265
+ if blocksize > size:
266
+ # this will buffer the whole thing
267
+ blocksize = size
268
+ super().__init__(blocksize, fetcher, size)
269
+ self.cache: bytes | None = None
270
+
271
+ def _fetch(self, start: int | None, end: int | None) -> bytes:
272
+ start = start or 0
273
+ if start > self.size:
274
+ logger.debug("FirstChunkCache: requested start > file size")
275
+ return b""
276
+
277
+ end = min(end, self.size)
278
+
279
+ if start < self.blocksize:
280
+ if self.cache is None:
281
+ self.miss_count += 1
282
+ if end > self.blocksize:
283
+ self.total_requested_bytes += end
284
+ data = self.fetcher(0, end)
285
+ self.cache = data[: self.blocksize]
286
+ return data[start:]
287
+ self.cache = self.fetcher(0, self.blocksize)
288
+ self.total_requested_bytes += self.blocksize
289
+ part = self.cache[start:end]
290
+ if end > self.blocksize:
291
+ self.total_requested_bytes += end - self.blocksize
292
+ part += self.fetcher(self.blocksize, end)
293
+ self.hit_count += 1
294
+ return part
295
+ else:
296
+ self.miss_count += 1
297
+ self.total_requested_bytes += end - start
298
+ return self.fetcher(start, end)
299
+
300
+
301
+ class BlockCache(BaseCache):
302
+ """
303
+ Cache holding memory as a set of blocks.
304
+
305
+ Requests are only ever made ``blocksize`` at a time, and are
306
+ stored in an LRU cache. The least recently accessed block is
307
+ discarded when more than ``maxblocks`` are stored.
308
+
309
+ Parameters
310
+ ----------
311
+ blocksize : int
312
+ The number of bytes to store in each block.
313
+ Requests are only ever made for ``blocksize``, so this
314
+ should balance the overhead of making a request against
315
+ the granularity of the blocks.
316
+ fetcher : Callable
317
+ size : int
318
+ The total size of the file being cached.
319
+ maxblocks : int
320
+ The maximum number of blocks to cache for. The maximum memory
321
+ use for this cache is then ``blocksize * maxblocks``.
322
+ """
323
+
324
+ name = "blockcache"
325
+
326
+ def __init__(
327
+ self, blocksize: int, fetcher: Fetcher, size: int, maxblocks: int = 32
328
+ ) -> None:
329
+ super().__init__(blocksize, fetcher, size)
330
+ self.nblocks = math.ceil(size / blocksize)
331
+ self.maxblocks = maxblocks
332
+ self._fetch_block_cached = functools.lru_cache(maxblocks)(self._fetch_block)
333
+
334
+ def cache_info(self):
335
+ """
336
+ The statistics on the block cache.
337
+
338
+ Returns
339
+ -------
340
+ NamedTuple
341
+ Returned directly from the LRU Cache used internally.
342
+ """
343
+ return self._fetch_block_cached.cache_info()
344
+
345
+ def __getstate__(self) -> dict[str, Any]:
346
+ state = self.__dict__
347
+ del state["_fetch_block_cached"]
348
+ return state
349
+
350
+ def __setstate__(self, state: dict[str, Any]) -> None:
351
+ self.__dict__.update(state)
352
+ self._fetch_block_cached = functools.lru_cache(state["maxblocks"])(
353
+ self._fetch_block
354
+ )
355
+
356
+ def _fetch(self, start: int | None, end: int | None) -> bytes:
357
+ if start is None:
358
+ start = 0
359
+ if end is None:
360
+ end = self.size
361
+ if start >= self.size or start >= end:
362
+ return b""
363
+
364
+ # byte position -> block numbers
365
+ start_block_number = start // self.blocksize
366
+ end_block_number = end // self.blocksize
367
+
368
+ # these are cached, so safe to do multiple calls for the same start and end.
369
+ for block_number in range(start_block_number, end_block_number + 1):
370
+ self._fetch_block_cached(block_number)
371
+
372
+ return self._read_cache(
373
+ start,
374
+ end,
375
+ start_block_number=start_block_number,
376
+ end_block_number=end_block_number,
377
+ )
378
+
379
+ def _fetch_block(self, block_number: int) -> bytes:
380
+ """
381
+ Fetch the block of data for `block_number`.
382
+ """
383
+ if block_number > self.nblocks:
384
+ raise ValueError(
385
+ f"'block_number={block_number}' is greater than "
386
+ f"the number of blocks ({self.nblocks})"
387
+ )
388
+
389
+ start = block_number * self.blocksize
390
+ end = start + self.blocksize
391
+ self.total_requested_bytes += end - start
392
+ self.miss_count += 1
393
+ logger.info("BlockCache fetching block %d", block_number)
394
+ block_contents = super()._fetch(start, end)
395
+ return block_contents
396
+
397
+ def _read_cache(
398
+ self, start: int, end: int, start_block_number: int, end_block_number: int
399
+ ) -> bytes:
400
+ """
401
+ Read from our block cache.
402
+
403
+ Parameters
404
+ ----------
405
+ start, end : int
406
+ The start and end byte positions.
407
+ start_block_number, end_block_number : int
408
+ The start and end block numbers.
409
+ """
410
+ start_pos = start % self.blocksize
411
+ end_pos = end % self.blocksize
412
+
413
+ self.hit_count += 1
414
+ if start_block_number == end_block_number:
415
+ block: bytes = self._fetch_block_cached(start_block_number)
416
+ return block[start_pos:end_pos]
417
+
418
+ else:
419
+ # read from the initial
420
+ out = [self._fetch_block_cached(start_block_number)[start_pos:]]
421
+
422
+ # intermediate blocks
423
+ # Note: it'd be nice to combine these into one big request. However
424
+ # that doesn't play nicely with our LRU cache.
425
+ out.extend(
426
+ map(
427
+ self._fetch_block_cached,
428
+ range(start_block_number + 1, end_block_number),
429
+ )
430
+ )
431
+
432
+ # final block
433
+ out.append(self._fetch_block_cached(end_block_number)[:end_pos])
434
+
435
+ return b"".join(out)
436
+
437
+
438
+ class BytesCache(BaseCache):
439
+ """Cache which holds data in a in-memory bytes object
440
+
441
+ Implements read-ahead by the block size, for semi-random reads progressing
442
+ through the file.
443
+
444
+ Parameters
445
+ ----------
446
+ trim: bool
447
+ As we read more data, whether to discard the start of the buffer when
448
+ we are more than a blocksize ahead of it.
449
+ """
450
+
451
+ name: ClassVar[str] = "bytes"
452
+
453
+ def __init__(
454
+ self, blocksize: int, fetcher: Fetcher, size: int, trim: bool = True
455
+ ) -> None:
456
+ super().__init__(blocksize, fetcher, size)
457
+ self.cache = b""
458
+ self.start: int | None = None
459
+ self.end: int | None = None
460
+ self.trim = trim
461
+
462
+ def _fetch(self, start: int | None, end: int | None) -> bytes:
463
+ # TODO: only set start/end after fetch, in case it fails?
464
+ # is this where retry logic might go?
465
+ if start is None:
466
+ start = 0
467
+ if end is None:
468
+ end = self.size
469
+ if start >= self.size or start >= end:
470
+ return b""
471
+ if (
472
+ self.start is not None
473
+ and start >= self.start
474
+ and self.end is not None
475
+ and end < self.end
476
+ ):
477
+ # cache hit: we have all the required data
478
+ offset = start - self.start
479
+ self.hit_count += 1
480
+ return self.cache[offset : offset + end - start]
481
+
482
+ if self.blocksize:
483
+ bend = min(self.size, end + self.blocksize)
484
+ else:
485
+ bend = end
486
+
487
+ if bend == start or start > self.size:
488
+ return b""
489
+
490
+ if (self.start is None or start < self.start) and (
491
+ self.end is None or end > self.end
492
+ ):
493
+ # First read, or extending both before and after
494
+ self.total_requested_bytes += bend - start
495
+ self.miss_count += 1
496
+ self.cache = self.fetcher(start, bend)
497
+ self.start = start
498
+ else:
499
+ assert self.start is not None
500
+ assert self.end is not None
501
+ self.miss_count += 1
502
+
503
+ if start < self.start:
504
+ if self.end is None or self.end - end > self.blocksize:
505
+ self.total_requested_bytes += bend - start
506
+ self.cache = self.fetcher(start, bend)
507
+ self.start = start
508
+ else:
509
+ self.total_requested_bytes += self.start - start
510
+ new = self.fetcher(start, self.start)
511
+ self.start = start
512
+ self.cache = new + self.cache
513
+ elif self.end is not None and bend > self.end:
514
+ if self.end > self.size:
515
+ pass
516
+ elif end - self.end > self.blocksize:
517
+ self.total_requested_bytes += bend - start
518
+ self.cache = self.fetcher(start, bend)
519
+ self.start = start
520
+ else:
521
+ self.total_requested_bytes += bend - self.end
522
+ new = self.fetcher(self.end, bend)
523
+ self.cache = self.cache + new
524
+
525
+ self.end = self.start + len(self.cache)
526
+ offset = start - self.start
527
+ out = self.cache[offset : offset + end - start]
528
+ if self.trim:
529
+ num = (self.end - self.start) // (self.blocksize + 1)
530
+ if num > 1:
531
+ self.start += self.blocksize * num
532
+ self.cache = self.cache[self.blocksize * num :]
533
+ return out
534
+
535
+ def __len__(self) -> int:
536
+ return len(self.cache)
537
+
538
+
539
+ class AllBytes(BaseCache):
540
+ """Cache entire contents of the file"""
541
+
542
+ name: ClassVar[str] = "all"
543
+
544
+ def __init__(
545
+ self,
546
+ blocksize: int | None = None,
547
+ fetcher: Fetcher | None = None,
548
+ size: int | None = None,
549
+ data: bytes | None = None,
550
+ ) -> None:
551
+ super().__init__(blocksize, fetcher, size) # type: ignore[arg-type]
552
+ if data is None:
553
+ self.miss_count += 1
554
+ self.total_requested_bytes += self.size
555
+ data = self.fetcher(0, self.size)
556
+ self.data = data
557
+
558
+ def _fetch(self, start: int | None, stop: int | None) -> bytes:
559
+ self.hit_count += 1
560
+ return self.data[start:stop]
561
+
562
+
563
+ class KnownPartsOfAFile(BaseCache):
564
+ """
565
+ Cache holding known file parts.
566
+
567
+ Parameters
568
+ ----------
569
+ blocksize: int
570
+ How far to read ahead in numbers of bytes
571
+ fetcher: func
572
+ Function of the form f(start, end) which gets bytes from remote as
573
+ specified
574
+ size: int
575
+ How big this file is
576
+ data: dict
577
+ A dictionary mapping explicit `(start, stop)` file-offset tuples
578
+ with known bytes.
579
+ strict: bool, default True
580
+ Whether to fetch reads that go beyond a known byte-range boundary.
581
+ If `False`, any read that ends outside a known part will be zero
582
+ padded. Note that zero padding will not be used for reads that
583
+ begin outside a known byte-range.
584
+ """
585
+
586
+ name: ClassVar[str] = "parts"
587
+
588
+ def __init__(
589
+ self,
590
+ blocksize: int,
591
+ fetcher: Fetcher,
592
+ size: int,
593
+ data: Optional[dict[tuple[int, int], bytes]] = None,
594
+ strict: bool = True,
595
+ **_: Any,
596
+ ):
597
+ super().__init__(blocksize, fetcher, size)
598
+ self.strict = strict
599
+
600
+ # simple consolidation of contiguous blocks
601
+ if data:
602
+ old_offsets = sorted(data.keys())
603
+ offsets = [old_offsets[0]]
604
+ blocks = [data.pop(old_offsets[0])]
605
+ for start, stop in old_offsets[1:]:
606
+ start0, stop0 = offsets[-1]
607
+ if start == stop0:
608
+ offsets[-1] = (start0, stop)
609
+ blocks[-1] += data.pop((start, stop))
610
+ else:
611
+ offsets.append((start, stop))
612
+ blocks.append(data.pop((start, stop)))
613
+
614
+ self.data = dict(zip(offsets, blocks))
615
+ else:
616
+ self.data = {}
617
+
618
+ def _fetch(self, start: int | None, stop: int | None) -> bytes:
619
+ if start is None:
620
+ start = 0
621
+ if stop is None:
622
+ stop = self.size
623
+
624
+ out = b""
625
+ for (loc0, loc1), data in self.data.items():
626
+ # If self.strict=False, use zero-padded data
627
+ # for reads beyond the end of a "known" buffer
628
+ if loc0 <= start < loc1:
629
+ off = start - loc0
630
+ out = data[off : off + stop - start]
631
+ if not self.strict or loc0 <= stop <= loc1:
632
+ # The request is within a known range, or
633
+ # it begins within a known range, and we
634
+ # are allowed to pad reads beyond the
635
+ # buffer with zero
636
+ out += b"\x00" * (stop - start - len(out))
637
+ self.hit_count += 1
638
+ return out
639
+ else:
640
+ # The request ends outside a known range,
641
+ # and we are being "strict" about reads
642
+ # beyond the buffer
643
+ start = loc1
644
+ break
645
+
646
+ # We only get here if there is a request outside the
647
+ # known parts of the file. In an ideal world, this
648
+ # should never happen
649
+ if self.fetcher is None:
650
+ # We cannot fetch the data, so raise an error
651
+ raise ValueError(f"Read is outside the known file parts: {(start, stop)}. ")
652
+ # We can fetch the data, but should warn the user
653
+ # that this may be slow
654
+ warnings.warn(
655
+ f"Read is outside the known file parts: {(start, stop)}. "
656
+ f"IO/caching performance may be poor!"
657
+ )
658
+ logger.debug(f"KnownPartsOfAFile cache fetching {start}-{stop}")
659
+ self.total_requested_bytes += stop - start
660
+ self.miss_count += 1
661
+ return out + super()._fetch(start, stop)
662
+
663
+
664
+ class UpdatableLRU(Generic[P, T]):
665
+ """
666
+ Custom implementation of LRU cache that allows updating keys
667
+
668
+ Used by BackgroudBlockCache
669
+ """
670
+
671
+ class CacheInfo(NamedTuple):
672
+ hits: int
673
+ misses: int
674
+ maxsize: int
675
+ currsize: int
676
+
677
+ def __init__(self, func: Callable[P, T], max_size: int = 128) -> None:
678
+ self._cache: OrderedDict[Any, T] = collections.OrderedDict()
679
+ self._func = func
680
+ self._max_size = max_size
681
+ self._hits = 0
682
+ self._misses = 0
683
+ self._lock = threading.Lock()
684
+
685
+ def __call__(self, *args: P.args, **kwargs: P.kwargs) -> T:
686
+ if kwargs:
687
+ raise TypeError(f"Got unexpected keyword argument {kwargs.keys()}")
688
+ with self._lock:
689
+ if args in self._cache:
690
+ self._cache.move_to_end(args)
691
+ self._hits += 1
692
+ return self._cache[args]
693
+
694
+ result = self._func(*args, **kwargs)
695
+
696
+ with self._lock:
697
+ self._cache[args] = result
698
+ self._misses += 1
699
+ if len(self._cache) > self._max_size:
700
+ self._cache.popitem(last=False)
701
+
702
+ return result
703
+
704
+ def is_key_cached(self, *args: Any) -> bool:
705
+ with self._lock:
706
+ return args in self._cache
707
+
708
+ def add_key(self, result: T, *args: Any) -> None:
709
+ with self._lock:
710
+ self._cache[args] = result
711
+ if len(self._cache) > self._max_size:
712
+ self._cache.popitem(last=False)
713
+
714
+ def cache_info(self) -> UpdatableLRU.CacheInfo:
715
+ with self._lock:
716
+ return self.CacheInfo(
717
+ maxsize=self._max_size,
718
+ currsize=len(self._cache),
719
+ hits=self._hits,
720
+ misses=self._misses,
721
+ )
722
+
723
+
724
+ class BackgroundBlockCache(BaseCache):
725
+ """
726
+ Cache holding memory as a set of blocks with pre-loading of
727
+ the next block in the background.
728
+
729
+ Requests are only ever made ``blocksize`` at a time, and are
730
+ stored in an LRU cache. The least recently accessed block is
731
+ discarded when more than ``maxblocks`` are stored. If the
732
+ next block is not in cache, it is loaded in a separate thread
733
+ in non-blocking way.
734
+
735
+ Parameters
736
+ ----------
737
+ blocksize : int
738
+ The number of bytes to store in each block.
739
+ Requests are only ever made for ``blocksize``, so this
740
+ should balance the overhead of making a request against
741
+ the granularity of the blocks.
742
+ fetcher : Callable
743
+ size : int
744
+ The total size of the file being cached.
745
+ maxblocks : int
746
+ The maximum number of blocks to cache for. The maximum memory
747
+ use for this cache is then ``blocksize * maxblocks``.
748
+ """
749
+
750
+ name: ClassVar[str] = "background"
751
+
752
+ def __init__(
753
+ self, blocksize: int, fetcher: Fetcher, size: int, maxblocks: int = 32
754
+ ) -> None:
755
+ super().__init__(blocksize, fetcher, size)
756
+ self.nblocks = math.ceil(size / blocksize)
757
+ self.maxblocks = maxblocks
758
+ self._fetch_block_cached = UpdatableLRU(self._fetch_block, maxblocks)
759
+
760
+ self._thread_executor = ThreadPoolExecutor(max_workers=1)
761
+ self._fetch_future_block_number: int | None = None
762
+ self._fetch_future: Future[bytes] | None = None
763
+ self._fetch_future_lock = threading.Lock()
764
+
765
+ def cache_info(self) -> UpdatableLRU.CacheInfo:
766
+ """
767
+ The statistics on the block cache.
768
+
769
+ Returns
770
+ -------
771
+ NamedTuple
772
+ Returned directly from the LRU Cache used internally.
773
+ """
774
+ return self._fetch_block_cached.cache_info()
775
+
776
+ def __getstate__(self) -> dict[str, Any]:
777
+ state = self.__dict__
778
+ del state["_fetch_block_cached"]
779
+ del state["_thread_executor"]
780
+ del state["_fetch_future_block_number"]
781
+ del state["_fetch_future"]
782
+ del state["_fetch_future_lock"]
783
+ return state
784
+
785
+ def __setstate__(self, state) -> None:
786
+ self.__dict__.update(state)
787
+ self._fetch_block_cached = UpdatableLRU(self._fetch_block, state["maxblocks"])
788
+ self._thread_executor = ThreadPoolExecutor(max_workers=1)
789
+ self._fetch_future_block_number = None
790
+ self._fetch_future = None
791
+ self._fetch_future_lock = threading.Lock()
792
+
793
+ def _fetch(self, start: int | None, end: int | None) -> bytes:
794
+ if start is None:
795
+ start = 0
796
+ if end is None:
797
+ end = self.size
798
+ if start >= self.size or start >= end:
799
+ return b""
800
+
801
+ # byte position -> block numbers
802
+ start_block_number = start // self.blocksize
803
+ end_block_number = end // self.blocksize
804
+
805
+ fetch_future_block_number = None
806
+ fetch_future = None
807
+ with self._fetch_future_lock:
808
+ # Background thread is running. Check we we can or must join it.
809
+ if self._fetch_future is not None:
810
+ assert self._fetch_future_block_number is not None
811
+ if self._fetch_future.done():
812
+ logger.info("BlockCache joined background fetch without waiting.")
813
+ self._fetch_block_cached.add_key(
814
+ self._fetch_future.result(), self._fetch_future_block_number
815
+ )
816
+ # Cleanup the fetch variables. Done with fetching the block.
817
+ self._fetch_future_block_number = None
818
+ self._fetch_future = None
819
+ else:
820
+ # Must join if we need the block for the current fetch
821
+ must_join = bool(
822
+ start_block_number
823
+ <= self._fetch_future_block_number
824
+ <= end_block_number
825
+ )
826
+ if must_join:
827
+ # Copy to the local variables to release lock
828
+ # before waiting for result
829
+ fetch_future_block_number = self._fetch_future_block_number
830
+ fetch_future = self._fetch_future
831
+
832
+ # Cleanup the fetch variables. Have a local copy.
833
+ self._fetch_future_block_number = None
834
+ self._fetch_future = None
835
+
836
+ # Need to wait for the future for the current read
837
+ if fetch_future is not None:
838
+ logger.info("BlockCache waiting for background fetch.")
839
+ # Wait until result and put it in cache
840
+ self._fetch_block_cached.add_key(
841
+ fetch_future.result(), fetch_future_block_number
842
+ )
843
+
844
+ # these are cached, so safe to do multiple calls for the same start and end.
845
+ for block_number in range(start_block_number, end_block_number + 1):
846
+ self._fetch_block_cached(block_number)
847
+
848
+ # fetch next block in the background if nothing is running in the background,
849
+ # the block is within file and it is not already cached
850
+ end_block_plus_1 = end_block_number + 1
851
+ with self._fetch_future_lock:
852
+ if (
853
+ self._fetch_future is None
854
+ and end_block_plus_1 <= self.nblocks
855
+ and not self._fetch_block_cached.is_key_cached(end_block_plus_1)
856
+ ):
857
+ self._fetch_future_block_number = end_block_plus_1
858
+ self._fetch_future = self._thread_executor.submit(
859
+ self._fetch_block, end_block_plus_1, "async"
860
+ )
861
+
862
+ return self._read_cache(
863
+ start,
864
+ end,
865
+ start_block_number=start_block_number,
866
+ end_block_number=end_block_number,
867
+ )
868
+
869
+ def _fetch_block(self, block_number: int, log_info: str = "sync") -> bytes:
870
+ """
871
+ Fetch the block of data for `block_number`.
872
+ """
873
+ if block_number > self.nblocks:
874
+ raise ValueError(
875
+ f"'block_number={block_number}' is greater than "
876
+ f"the number of blocks ({self.nblocks})"
877
+ )
878
+
879
+ start = block_number * self.blocksize
880
+ end = start + self.blocksize
881
+ logger.info("BlockCache fetching block (%s) %d", log_info, block_number)
882
+ self.total_requested_bytes += end - start
883
+ self.miss_count += 1
884
+ block_contents = super()._fetch(start, end)
885
+ return block_contents
886
+
887
+ def _read_cache(
888
+ self, start: int, end: int, start_block_number: int, end_block_number: int
889
+ ) -> bytes:
890
+ """
891
+ Read from our block cache.
892
+
893
+ Parameters
894
+ ----------
895
+ start, end : int
896
+ The start and end byte positions.
897
+ start_block_number, end_block_number : int
898
+ The start and end block numbers.
899
+ """
900
+ start_pos = start % self.blocksize
901
+ end_pos = end % self.blocksize
902
+
903
+ # kind of pointless to count this as a hit, but it is
904
+ self.hit_count += 1
905
+
906
+ if start_block_number == end_block_number:
907
+ block = self._fetch_block_cached(start_block_number)
908
+ return block[start_pos:end_pos]
909
+
910
+ else:
911
+ # read from the initial
912
+ out = [self._fetch_block_cached(start_block_number)[start_pos:]]
913
+
914
+ # intermediate blocks
915
+ # Note: it'd be nice to combine these into one big request. However
916
+ # that doesn't play nicely with our LRU cache.
917
+ out.extend(
918
+ map(
919
+ self._fetch_block_cached,
920
+ range(start_block_number + 1, end_block_number),
921
+ )
922
+ )
923
+
924
+ # final block
925
+ out.append(self._fetch_block_cached(end_block_number)[:end_pos])
926
+
927
+ return b"".join(out)
928
+
929
+
930
+ caches: dict[str | None, type[BaseCache]] = {
931
+ # one custom case
932
+ None: BaseCache,
933
+ }
934
+
935
+
936
+ def register_cache(cls: type[BaseCache], clobber: bool = False) -> None:
937
+ """'Register' cache implementation.
938
+
939
+ Parameters
940
+ ----------
941
+ clobber: bool, optional
942
+ If set to True (default is False) - allow to overwrite existing
943
+ entry.
944
+
945
+ Raises
946
+ ------
947
+ ValueError
948
+ """
949
+ name = cls.name
950
+ if not clobber and name in caches:
951
+ raise ValueError(f"Cache with name {name!r} is already known: {caches[name]}")
952
+ caches[name] = cls
953
+
954
+
955
+ for c in (
956
+ BaseCache,
957
+ MMapCache,
958
+ BytesCache,
959
+ ReadAheadCache,
960
+ BlockCache,
961
+ FirstChunkCache,
962
+ AllBytes,
963
+ KnownPartsOfAFile,
964
+ BackgroundBlockCache,
965
+ ):
966
+ register_cache(c)
lib/python3.12/site-packages/fsspec/callbacks.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from functools import wraps
2
+
3
+
4
+ class Callback:
5
+ """
6
+ Base class and interface for callback mechanism
7
+
8
+ This class can be used directly for monitoring file transfers by
9
+ providing ``callback=Callback(hooks=...)`` (see the ``hooks`` argument,
10
+ below), or subclassed for more specialised behaviour.
11
+
12
+ Parameters
13
+ ----------
14
+ size: int (optional)
15
+ Nominal quantity for the value that corresponds to a complete
16
+ transfer, e.g., total number of tiles or total number of
17
+ bytes
18
+ value: int (0)
19
+ Starting internal counter value
20
+ hooks: dict or None
21
+ A dict of named functions to be called on each update. The signature
22
+ of these must be ``f(size, value, **kwargs)``
23
+ """
24
+
25
+ def __init__(self, size=None, value=0, hooks=None, **kwargs):
26
+ self.size = size
27
+ self.value = value
28
+ self.hooks = hooks or {}
29
+ self.kw = kwargs
30
+
31
+ def __enter__(self):
32
+ return self
33
+
34
+ def __exit__(self, *exc_args):
35
+ self.close()
36
+
37
+ def close(self):
38
+ """Close callback."""
39
+
40
+ def branched(self, path_1, path_2, **kwargs):
41
+ """
42
+ Return callback for child transfers
43
+
44
+ If this callback is operating at a higher level, e.g., put, which may
45
+ trigger transfers that can also be monitored. The function returns a callback
46
+ that has to be passed to the child method, e.g., put_file,
47
+ as `callback=` argument.
48
+
49
+ The implementation uses `callback.branch` for compatibility.
50
+ When implementing callbacks, it is recommended to override this function instead
51
+ of `branch` and avoid calling `super().branched(...)`.
52
+
53
+ Prefer using this function over `branch`.
54
+
55
+ Parameters
56
+ ----------
57
+ path_1: str
58
+ Child's source path
59
+ path_2: str
60
+ Child's destination path
61
+ **kwargs:
62
+ Arbitrary keyword arguments
63
+
64
+ Returns
65
+ -------
66
+ callback: Callback
67
+ A callback instance to be passed to the child method
68
+ """
69
+ self.branch(path_1, path_2, kwargs)
70
+ # mutate kwargs so that we can force the caller to pass "callback=" explicitly
71
+ return kwargs.pop("callback", DEFAULT_CALLBACK)
72
+
73
+ def branch_coro(self, fn):
74
+ """
75
+ Wraps a coroutine, and pass a new child callback to it.
76
+ """
77
+
78
+ @wraps(fn)
79
+ async def func(path1, path2: str, **kwargs):
80
+ with self.branched(path1, path2, **kwargs) as child:
81
+ return await fn(path1, path2, callback=child, **kwargs)
82
+
83
+ return func
84
+
85
+ def set_size(self, size):
86
+ """
87
+ Set the internal maximum size attribute
88
+
89
+ Usually called if not initially set at instantiation. Note that this
90
+ triggers a ``call()``.
91
+
92
+ Parameters
93
+ ----------
94
+ size: int
95
+ """
96
+ self.size = size
97
+ self.call()
98
+
99
+ def absolute_update(self, value):
100
+ """
101
+ Set the internal value state
102
+
103
+ Triggers ``call()``
104
+
105
+ Parameters
106
+ ----------
107
+ value: int
108
+ """
109
+ self.value = value
110
+ self.call()
111
+
112
+ def relative_update(self, inc=1):
113
+ """
114
+ Delta increment the internal counter
115
+
116
+ Triggers ``call()``
117
+
118
+ Parameters
119
+ ----------
120
+ inc: int
121
+ """
122
+ self.value += inc
123
+ self.call()
124
+
125
+ def call(self, hook_name=None, **kwargs):
126
+ """
127
+ Execute hook(s) with current state
128
+
129
+ Each function is passed the internal size and current value
130
+
131
+ Parameters
132
+ ----------
133
+ hook_name: str or None
134
+ If given, execute on this hook
135
+ kwargs: passed on to (all) hook(s)
136
+ """
137
+ if not self.hooks:
138
+ return
139
+ kw = self.kw.copy()
140
+ kw.update(kwargs)
141
+ if hook_name:
142
+ if hook_name not in self.hooks:
143
+ return
144
+ return self.hooks[hook_name](self.size, self.value, **kw)
145
+ for hook in self.hooks.values() or []:
146
+ hook(self.size, self.value, **kw)
147
+
148
+ def wrap(self, iterable):
149
+ """
150
+ Wrap an iterable to call ``relative_update`` on each iterations
151
+
152
+ Parameters
153
+ ----------
154
+ iterable: Iterable
155
+ The iterable that is being wrapped
156
+ """
157
+ for item in iterable:
158
+ self.relative_update()
159
+ yield item
160
+
161
+ def branch(self, path_1, path_2, kwargs):
162
+ """
163
+ Set callbacks for child transfers
164
+
165
+ If this callback is operating at a higher level, e.g., put, which may
166
+ trigger transfers that can also be monitored. The passed kwargs are
167
+ to be *mutated* to add ``callback=``, if this class supports branching
168
+ to children.
169
+
170
+ Parameters
171
+ ----------
172
+ path_1: str
173
+ Child's source path
174
+ path_2: str
175
+ Child's destination path
176
+ kwargs: dict
177
+ arguments passed to child method, e.g., put_file.
178
+
179
+ Returns
180
+ -------
181
+
182
+ """
183
+ return None
184
+
185
+ def no_op(self, *_, **__):
186
+ pass
187
+
188
+ def __getattr__(self, item):
189
+ """
190
+ If undefined methods are called on this class, nothing happens
191
+ """
192
+ return self.no_op
193
+
194
+ @classmethod
195
+ def as_callback(cls, maybe_callback=None):
196
+ """Transform callback=... into Callback instance
197
+
198
+ For the special value of ``None``, return the global instance of
199
+ ``NoOpCallback``. This is an alternative to including
200
+ ``callback=DEFAULT_CALLBACK`` directly in a method signature.
201
+ """
202
+ if maybe_callback is None:
203
+ return DEFAULT_CALLBACK
204
+ return maybe_callback
205
+
206
+
207
+ class NoOpCallback(Callback):
208
+ """
209
+ This implementation of Callback does exactly nothing
210
+ """
211
+
212
+ def call(self, *args, **kwargs):
213
+ return None
214
+
215
+
216
+ class DotPrinterCallback(Callback):
217
+ """
218
+ Simple example Callback implementation
219
+
220
+ Almost identical to Callback with a hook that prints a char; here we
221
+ demonstrate how the outer layer may print "#" and the inner layer "."
222
+ """
223
+
224
+ def __init__(self, chr_to_print="#", **kwargs):
225
+ self.chr = chr_to_print
226
+ super().__init__(**kwargs)
227
+
228
+ def branch(self, path_1, path_2, kwargs):
229
+ """Mutate kwargs to add new instance with different print char"""
230
+ kwargs["callback"] = DotPrinterCallback(".")
231
+
232
+ def call(self, **kwargs):
233
+ """Just outputs a character"""
234
+ print(self.chr, end="")
235
+
236
+
237
+ class TqdmCallback(Callback):
238
+ """
239
+ A callback to display a progress bar using tqdm
240
+
241
+ Parameters
242
+ ----------
243
+ tqdm_kwargs : dict, (optional)
244
+ Any argument accepted by the tqdm constructor.
245
+ See the `tqdm doc <https://tqdm.github.io/docs/tqdm/#__init__>`_.
246
+ Will be forwarded to `tqdm_cls`.
247
+ tqdm_cls: (optional)
248
+ subclass of `tqdm.tqdm`. If not passed, it will default to `tqdm.tqdm`.
249
+
250
+ Examples
251
+ --------
252
+ >>> import fsspec
253
+ >>> from fsspec.callbacks import TqdmCallback
254
+ >>> fs = fsspec.filesystem("memory")
255
+ >>> path2distant_data = "/your-path"
256
+ >>> fs.upload(
257
+ ".",
258
+ path2distant_data,
259
+ recursive=True,
260
+ callback=TqdmCallback(),
261
+ )
262
+
263
+ You can forward args to tqdm using the ``tqdm_kwargs`` parameter.
264
+
265
+ >>> fs.upload(
266
+ ".",
267
+ path2distant_data,
268
+ recursive=True,
269
+ callback=TqdmCallback(tqdm_kwargs={"desc": "Your tqdm description"}),
270
+ )
271
+
272
+ You can also customize the progress bar by passing a subclass of `tqdm`.
273
+
274
+ .. code-block:: python
275
+
276
+ class TqdmFormat(tqdm):
277
+ '''Provides a `total_time` format parameter'''
278
+ @property
279
+ def format_dict(self):
280
+ d = super().format_dict
281
+ total_time = d["elapsed"] * (d["total"] or 0) / max(d["n"], 1)
282
+ d.update(total_time=self.format_interval(total_time) + " in total")
283
+ return d
284
+
285
+ >>> with TqdmCallback(
286
+ tqdm_kwargs={
287
+ "desc": "desc",
288
+ "bar_format": "{total_time}: {percentage:.0f}%|{bar}{r_bar}",
289
+ },
290
+ tqdm_cls=TqdmFormat,
291
+ ) as callback:
292
+ fs.upload(".", path2distant_data, recursive=True, callback=callback)
293
+ """
294
+
295
+ def __init__(self, tqdm_kwargs=None, *args, **kwargs):
296
+ try:
297
+ from tqdm import tqdm
298
+
299
+ except ImportError as exce:
300
+ raise ImportError(
301
+ "Using TqdmCallback requires tqdm to be installed"
302
+ ) from exce
303
+
304
+ self._tqdm_cls = kwargs.pop("tqdm_cls", tqdm)
305
+ self._tqdm_kwargs = tqdm_kwargs or {}
306
+ self.tqdm = None
307
+ super().__init__(*args, **kwargs)
308
+
309
+ def call(self, *args, **kwargs):
310
+ if self.tqdm is None:
311
+ self.tqdm = self._tqdm_cls(total=self.size, **self._tqdm_kwargs)
312
+ self.tqdm.total = self.size
313
+ self.tqdm.update(self.value - self.tqdm.n)
314
+
315
+ def close(self):
316
+ if self.tqdm is not None:
317
+ self.tqdm.close()
318
+ self.tqdm = None
319
+
320
+ def __del__(self):
321
+ return self.close()
322
+
323
+
324
+ DEFAULT_CALLBACK = _DEFAULT_CALLBACK = NoOpCallback()
lib/python3.12/site-packages/fsspec/compression.py ADDED
@@ -0,0 +1,175 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Helper functions for a standard streaming compression API"""
2
+
3
+ from zipfile import ZipFile
4
+
5
+ import fsspec.utils
6
+ from fsspec.spec import AbstractBufferedFile
7
+
8
+
9
+ def noop_file(file, mode, **kwargs):
10
+ return file
11
+
12
+
13
+ # TODO: files should also be available as contexts
14
+ # should be functions of the form func(infile, mode=, **kwargs) -> file-like
15
+ compr = {None: noop_file}
16
+
17
+
18
+ def register_compression(name, callback, extensions, force=False):
19
+ """Register an "inferable" file compression type.
20
+
21
+ Registers transparent file compression type for use with fsspec.open.
22
+ Compression can be specified by name in open, or "infer"-ed for any files
23
+ ending with the given extensions.
24
+
25
+ Args:
26
+ name: (str) The compression type name. Eg. "gzip".
27
+ callback: A callable of form (infile, mode, **kwargs) -> file-like.
28
+ Accepts an input file-like object, the target mode and kwargs.
29
+ Returns a wrapped file-like object.
30
+ extensions: (str, Iterable[str]) A file extension, or list of file
31
+ extensions for which to infer this compression scheme. Eg. "gz".
32
+ force: (bool) Force re-registration of compression type or extensions.
33
+
34
+ Raises:
35
+ ValueError: If name or extensions already registered, and not force.
36
+
37
+ """
38
+ if isinstance(extensions, str):
39
+ extensions = [extensions]
40
+
41
+ # Validate registration
42
+ if name in compr and not force:
43
+ raise ValueError(f"Duplicate compression registration: {name}")
44
+
45
+ for ext in extensions:
46
+ if ext in fsspec.utils.compressions and not force:
47
+ raise ValueError(f"Duplicate compression file extension: {ext} ({name})")
48
+
49
+ compr[name] = callback
50
+
51
+ for ext in extensions:
52
+ fsspec.utils.compressions[ext] = name
53
+
54
+
55
+ def unzip(infile, mode="rb", filename=None, **kwargs):
56
+ if "r" not in mode:
57
+ filename = filename or "file"
58
+ z = ZipFile(infile, mode="w", **kwargs)
59
+ fo = z.open(filename, mode="w")
60
+ fo.close = lambda closer=fo.close: closer() or z.close()
61
+ return fo
62
+ z = ZipFile(infile)
63
+ if filename is None:
64
+ filename = z.namelist()[0]
65
+ return z.open(filename, mode="r", **kwargs)
66
+
67
+
68
+ register_compression("zip", unzip, "zip")
69
+
70
+ try:
71
+ from bz2 import BZ2File
72
+ except ImportError:
73
+ pass
74
+ else:
75
+ register_compression("bz2", BZ2File, "bz2")
76
+
77
+ try: # pragma: no cover
78
+ from isal import igzip
79
+
80
+ def isal(infile, mode="rb", **kwargs):
81
+ return igzip.IGzipFile(fileobj=infile, mode=mode, **kwargs)
82
+
83
+ register_compression("gzip", isal, "gz")
84
+ except ImportError:
85
+ from gzip import GzipFile
86
+
87
+ register_compression(
88
+ "gzip", lambda f, **kwargs: GzipFile(fileobj=f, **kwargs), "gz"
89
+ )
90
+
91
+ try:
92
+ from lzma import LZMAFile
93
+
94
+ register_compression("lzma", LZMAFile, "lzma")
95
+ register_compression("xz", LZMAFile, "xz")
96
+ except ImportError:
97
+ pass
98
+
99
+ try:
100
+ import lzmaffi
101
+
102
+ register_compression("lzma", lzmaffi.LZMAFile, "lzma", force=True)
103
+ register_compression("xz", lzmaffi.LZMAFile, "xz", force=True)
104
+ except ImportError:
105
+ pass
106
+
107
+
108
+ class SnappyFile(AbstractBufferedFile):
109
+ def __init__(self, infile, mode, **kwargs):
110
+ import snappy
111
+
112
+ super().__init__(
113
+ fs=None, path="snappy", mode=mode.strip("b") + "b", size=999999999, **kwargs
114
+ )
115
+ self.infile = infile
116
+ if "r" in mode:
117
+ self.codec = snappy.StreamDecompressor()
118
+ else:
119
+ self.codec = snappy.StreamCompressor()
120
+
121
+ def _upload_chunk(self, final=False):
122
+ self.buffer.seek(0)
123
+ out = self.codec.add_chunk(self.buffer.read())
124
+ self.infile.write(out)
125
+ return True
126
+
127
+ def seek(self, loc, whence=0):
128
+ raise NotImplementedError("SnappyFile is not seekable")
129
+
130
+ def seekable(self):
131
+ return False
132
+
133
+ def _fetch_range(self, start, end):
134
+ """Get the specified set of bytes from remote"""
135
+ data = self.infile.read(end - start)
136
+ return self.codec.decompress(data)
137
+
138
+
139
+ try:
140
+ import snappy
141
+
142
+ snappy.compress(b"")
143
+ # Snappy may use the .sz file extension, but this is not part of the
144
+ # standard implementation.
145
+ register_compression("snappy", SnappyFile, [])
146
+
147
+ except (ImportError, NameError, AttributeError):
148
+ pass
149
+
150
+ try:
151
+ import lz4.frame
152
+
153
+ register_compression("lz4", lz4.frame.open, "lz4")
154
+ except ImportError:
155
+ pass
156
+
157
+ try:
158
+ import zstandard as zstd
159
+
160
+ def zstandard_file(infile, mode="rb"):
161
+ if "r" in mode:
162
+ cctx = zstd.ZstdDecompressor()
163
+ return cctx.stream_reader(infile)
164
+ else:
165
+ cctx = zstd.ZstdCompressor(level=10)
166
+ return cctx.stream_writer(infile)
167
+
168
+ register_compression("zstd", zstandard_file, "zst")
169
+ except ImportError:
170
+ pass
171
+
172
+
173
+ def available_compressions():
174
+ """Return a list of the implemented compressions."""
175
+ return list(compr)
lib/python3.12/site-packages/fsspec/config.py ADDED
@@ -0,0 +1,131 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import configparser
4
+ import json
5
+ import os
6
+ import warnings
7
+ from typing import Any
8
+
9
+ conf: dict[str, dict[str, Any]] = {}
10
+ default_conf_dir = os.path.join(os.path.expanduser("~"), ".config/fsspec")
11
+ conf_dir = os.environ.get("FSSPEC_CONFIG_DIR", default_conf_dir)
12
+
13
+
14
+ def set_conf_env(conf_dict, envdict=os.environ):
15
+ """Set config values from environment variables
16
+
17
+ Looks for variables of the form ``FSSPEC_<protocol>`` and
18
+ ``FSSPEC_<protocol>_<kwarg>``. For ``FSSPEC_<protocol>`` the value is parsed
19
+ as a json dictionary and used to ``update`` the config of the
20
+ corresponding protocol. For ``FSSPEC_<protocol>_<kwarg>`` there is no
21
+ attempt to convert the string value, but the kwarg keys will be lower-cased.
22
+
23
+ The ``FSSPEC_<protocol>_<kwarg>`` variables are applied after the
24
+ ``FSSPEC_<protocol>`` ones.
25
+
26
+ Parameters
27
+ ----------
28
+ conf_dict : dict(str, dict)
29
+ This dict will be mutated
30
+ envdict : dict-like(str, str)
31
+ Source for the values - usually the real environment
32
+ """
33
+ kwarg_keys = []
34
+ for key in envdict:
35
+ if key.startswith("FSSPEC_") and len(key) > 7 and key[7] != "_":
36
+ if key.count("_") > 1:
37
+ kwarg_keys.append(key)
38
+ continue
39
+ try:
40
+ value = json.loads(envdict[key])
41
+ except json.decoder.JSONDecodeError as ex:
42
+ warnings.warn(
43
+ f"Ignoring environment variable {key} due to a parse failure: {ex}"
44
+ )
45
+ else:
46
+ if isinstance(value, dict):
47
+ _, proto = key.split("_", 1)
48
+ conf_dict.setdefault(proto.lower(), {}).update(value)
49
+ else:
50
+ warnings.warn(
51
+ f"Ignoring environment variable {key} due to not being a dict:"
52
+ f" {type(value)}"
53
+ )
54
+ elif key.startswith("FSSPEC"):
55
+ warnings.warn(
56
+ f"Ignoring environment variable {key} due to having an unexpected name"
57
+ )
58
+
59
+ for key in kwarg_keys:
60
+ _, proto, kwarg = key.split("_", 2)
61
+ conf_dict.setdefault(proto.lower(), {})[kwarg.lower()] = envdict[key]
62
+
63
+
64
+ def set_conf_files(cdir, conf_dict):
65
+ """Set config values from files
66
+
67
+ Scans for INI and JSON files in the given dictionary, and uses their
68
+ contents to set the config. In case of repeated values, later values
69
+ win.
70
+
71
+ In the case of INI files, all values are strings, and these will not
72
+ be converted.
73
+
74
+ Parameters
75
+ ----------
76
+ cdir : str
77
+ Directory to search
78
+ conf_dict : dict(str, dict)
79
+ This dict will be mutated
80
+ """
81
+ if not os.path.isdir(cdir):
82
+ return
83
+ allfiles = sorted(os.listdir(cdir))
84
+ for fn in allfiles:
85
+ if fn.endswith(".ini"):
86
+ ini = configparser.ConfigParser()
87
+ ini.read(os.path.join(cdir, fn))
88
+ for key in ini:
89
+ if key == "DEFAULT":
90
+ continue
91
+ conf_dict.setdefault(key, {}).update(dict(ini[key]))
92
+ if fn.endswith(".json"):
93
+ with open(os.path.join(cdir, fn)) as f:
94
+ js = json.load(f)
95
+ for key in js:
96
+ conf_dict.setdefault(key, {}).update(dict(js[key]))
97
+
98
+
99
+ def apply_config(cls, kwargs, conf_dict=None):
100
+ """Supply default values for kwargs when instantiating class
101
+
102
+ Augments the passed kwargs, by finding entries in the config dict
103
+ which match the classes ``.protocol`` attribute (one or more str)
104
+
105
+ Parameters
106
+ ----------
107
+ cls : file system implementation
108
+ kwargs : dict
109
+ conf_dict : dict of dict
110
+ Typically this is the global configuration
111
+
112
+ Returns
113
+ -------
114
+ dict : the modified set of kwargs
115
+ """
116
+ if conf_dict is None:
117
+ conf_dict = conf
118
+ protos = cls.protocol if isinstance(cls.protocol, (tuple, list)) else [cls.protocol]
119
+ kw = {}
120
+ for proto in protos:
121
+ # default kwargs from the current state of the config
122
+ if proto in conf_dict:
123
+ kw.update(conf_dict[proto])
124
+ # explicit kwargs always win
125
+ kw.update(**kwargs)
126
+ kwargs = kw
127
+ return kwargs
128
+
129
+
130
+ set_conf_files(conf_dir, conf)
131
+ set_conf_env(conf)
lib/python3.12/site-packages/fsspec/conftest.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import shutil
3
+ import subprocess
4
+ import sys
5
+ import time
6
+
7
+ import pytest
8
+
9
+ import fsspec
10
+ from fsspec.implementations.cached import CachingFileSystem
11
+
12
+
13
+ @pytest.fixture()
14
+ def m():
15
+ """
16
+ Fixture providing a memory filesystem.
17
+ """
18
+ m = fsspec.filesystem("memory")
19
+ m.store.clear()
20
+ m.pseudo_dirs.clear()
21
+ m.pseudo_dirs.append("")
22
+ try:
23
+ yield m
24
+ finally:
25
+ m.store.clear()
26
+ m.pseudo_dirs.clear()
27
+ m.pseudo_dirs.append("")
28
+
29
+
30
+ @pytest.fixture
31
+ def ftp_writable(tmpdir):
32
+ """
33
+ Fixture providing a writable FTP filesystem.
34
+ """
35
+ pytest.importorskip("pyftpdlib")
36
+ from fsspec.implementations.ftp import FTPFileSystem
37
+
38
+ FTPFileSystem.clear_instance_cache() # remove lingering connections
39
+ CachingFileSystem.clear_instance_cache()
40
+ d = str(tmpdir)
41
+ with open(os.path.join(d, "out"), "wb") as f:
42
+ f.write(b"hello" * 10000)
43
+ P = subprocess.Popen(
44
+ [sys.executable, "-m", "pyftpdlib", "-d", d, "-u", "user", "-P", "pass", "-w"]
45
+ )
46
+ try:
47
+ time.sleep(1)
48
+ yield "localhost", 2121, "user", "pass"
49
+ finally:
50
+ P.terminate()
51
+ P.wait()
52
+ try:
53
+ shutil.rmtree(tmpdir)
54
+ except Exception:
55
+ pass
lib/python3.12/site-packages/fsspec/core.py ADDED
@@ -0,0 +1,743 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import io
4
+ import logging
5
+ import os
6
+ import re
7
+ from glob import has_magic
8
+ from pathlib import Path
9
+
10
+ # for backwards compat, we export cache things from here too
11
+ from fsspec.caching import ( # noqa: F401
12
+ BaseCache,
13
+ BlockCache,
14
+ BytesCache,
15
+ MMapCache,
16
+ ReadAheadCache,
17
+ caches,
18
+ )
19
+ from fsspec.compression import compr
20
+ from fsspec.config import conf
21
+ from fsspec.registry import filesystem, get_filesystem_class
22
+ from fsspec.utils import (
23
+ _unstrip_protocol,
24
+ build_name_function,
25
+ infer_compression,
26
+ stringify_path,
27
+ )
28
+
29
+ logger = logging.getLogger("fsspec")
30
+
31
+
32
+ class OpenFile:
33
+ """
34
+ File-like object to be used in a context
35
+
36
+ Can layer (buffered) text-mode and compression over any file-system, which
37
+ are typically binary-only.
38
+
39
+ These instances are safe to serialize, as the low-level file object
40
+ is not created until invoked using ``with``.
41
+
42
+ Parameters
43
+ ----------
44
+ fs: FileSystem
45
+ The file system to use for opening the file. Should be a subclass or duck-type
46
+ with ``fsspec.spec.AbstractFileSystem``
47
+ path: str
48
+ Location to open
49
+ mode: str like 'rb', optional
50
+ Mode of the opened file
51
+ compression: str or None, optional
52
+ Compression to apply
53
+ encoding: str or None, optional
54
+ The encoding to use if opened in text mode.
55
+ errors: str or None, optional
56
+ How to handle encoding errors if opened in text mode.
57
+ newline: None or str
58
+ Passed to TextIOWrapper in text mode, how to handle line endings.
59
+ autoopen: bool
60
+ If True, calls open() immediately. Mostly used by pickle
61
+ pos: int
62
+ If given and autoopen is True, seek to this location immediately
63
+ """
64
+
65
+ def __init__(
66
+ self,
67
+ fs,
68
+ path,
69
+ mode="rb",
70
+ compression=None,
71
+ encoding=None,
72
+ errors=None,
73
+ newline=None,
74
+ ):
75
+ self.fs = fs
76
+ self.path = path
77
+ self.mode = mode
78
+ self.compression = get_compression(path, compression)
79
+ self.encoding = encoding
80
+ self.errors = errors
81
+ self.newline = newline
82
+ self.fobjects = []
83
+
84
+ def __reduce__(self):
85
+ return (
86
+ OpenFile,
87
+ (
88
+ self.fs,
89
+ self.path,
90
+ self.mode,
91
+ self.compression,
92
+ self.encoding,
93
+ self.errors,
94
+ self.newline,
95
+ ),
96
+ )
97
+
98
+ def __repr__(self):
99
+ return f"<OpenFile '{self.path}'>"
100
+
101
+ def __enter__(self):
102
+ mode = self.mode.replace("t", "").replace("b", "") + "b"
103
+
104
+ try:
105
+ f = self.fs.open(self.path, mode=mode)
106
+ except FileNotFoundError as e:
107
+ if has_magic(self.path):
108
+ raise FileNotFoundError(
109
+ "%s not found. The URL contains glob characters: you maybe needed\n"
110
+ "to pass expand=True in fsspec.open() or the storage_options of \n"
111
+ "your library. You can also set the config value 'open_expand'\n"
112
+ "before import, or fsspec.core.DEFAULT_EXPAND at runtime, to True.",
113
+ self.path,
114
+ ) from e
115
+ raise
116
+
117
+ self.fobjects = [f]
118
+
119
+ if self.compression is not None:
120
+ compress = compr[self.compression]
121
+ f = compress(f, mode=mode[0])
122
+ self.fobjects.append(f)
123
+
124
+ if "b" not in self.mode:
125
+ # assume, for example, that 'r' is equivalent to 'rt' as in builtin
126
+ f = PickleableTextIOWrapper(
127
+ f, encoding=self.encoding, errors=self.errors, newline=self.newline
128
+ )
129
+ self.fobjects.append(f)
130
+
131
+ return self.fobjects[-1]
132
+
133
+ def __exit__(self, *args):
134
+ self.close()
135
+
136
+ @property
137
+ def full_name(self):
138
+ return _unstrip_protocol(self.path, self.fs)
139
+
140
+ def open(self):
141
+ """Materialise this as a real open file without context
142
+
143
+ The OpenFile object should be explicitly closed to avoid enclosed file
144
+ instances persisting. You must, therefore, keep a reference to the OpenFile
145
+ during the life of the file-like it generates.
146
+ """
147
+ return self.__enter__()
148
+
149
+ def close(self):
150
+ """Close all encapsulated file objects"""
151
+ for f in reversed(self.fobjects):
152
+ if "r" not in self.mode and not f.closed:
153
+ f.flush()
154
+ f.close()
155
+ self.fobjects.clear()
156
+
157
+
158
+ class OpenFiles(list):
159
+ """List of OpenFile instances
160
+
161
+ Can be used in a single context, which opens and closes all of the
162
+ contained files. Normal list access to get the elements works as
163
+ normal.
164
+
165
+ A special case is made for caching filesystems - the files will
166
+ be down/uploaded together at the start or end of the context, and
167
+ this may happen concurrently, if the target filesystem supports it.
168
+ """
169
+
170
+ def __init__(self, *args, mode="rb", fs=None):
171
+ self.mode = mode
172
+ self.fs = fs
173
+ self.files = []
174
+ super().__init__(*args)
175
+
176
+ def __enter__(self):
177
+ if self.fs is None:
178
+ raise ValueError("Context has already been used")
179
+
180
+ fs = self.fs
181
+ while True:
182
+ if hasattr(fs, "open_many"):
183
+ # check for concurrent cache download; or set up for upload
184
+ self.files = fs.open_many(self)
185
+ return self.files
186
+ if hasattr(fs, "fs") and fs.fs is not None:
187
+ fs = fs.fs
188
+ else:
189
+ break
190
+ return [s.__enter__() for s in self]
191
+
192
+ def __exit__(self, *args):
193
+ fs = self.fs
194
+ [s.__exit__(*args) for s in self]
195
+ if "r" not in self.mode:
196
+ while True:
197
+ if hasattr(fs, "open_many"):
198
+ # check for concurrent cache upload
199
+ fs.commit_many(self.files)
200
+ return
201
+ if hasattr(fs, "fs") and fs.fs is not None:
202
+ fs = fs.fs
203
+ else:
204
+ break
205
+
206
+ def __getitem__(self, item):
207
+ out = super().__getitem__(item)
208
+ if isinstance(item, slice):
209
+ return OpenFiles(out, mode=self.mode, fs=self.fs)
210
+ return out
211
+
212
+ def __repr__(self):
213
+ return f"<List of {len(self)} OpenFile instances>"
214
+
215
+
216
+ def open_files(
217
+ urlpath,
218
+ mode="rb",
219
+ compression=None,
220
+ encoding="utf8",
221
+ errors=None,
222
+ name_function=None,
223
+ num=1,
224
+ protocol=None,
225
+ newline=None,
226
+ auto_mkdir=True,
227
+ expand=True,
228
+ **kwargs,
229
+ ):
230
+ """Given a path or paths, return a list of ``OpenFile`` objects.
231
+
232
+ For writing, a str path must contain the "*" character, which will be filled
233
+ in by increasing numbers, e.g., "part*" -> "part1", "part2" if num=2.
234
+
235
+ For either reading or writing, can instead provide explicit list of paths.
236
+
237
+ Parameters
238
+ ----------
239
+ urlpath: string or list
240
+ Absolute or relative filepath(s). Prefix with a protocol like ``s3://``
241
+ to read from alternative filesystems. To read from multiple files you
242
+ can pass a globstring or a list of paths, with the caveat that they
243
+ must all have the same protocol.
244
+ mode: 'rb', 'wt', etc.
245
+ compression: string or None
246
+ If given, open file using compression codec. Can either be a compression
247
+ name (a key in ``fsspec.compression.compr``) or "infer" to guess the
248
+ compression from the filename suffix.
249
+ encoding: str
250
+ For text mode only
251
+ errors: None or str
252
+ Passed to TextIOWrapper in text mode
253
+ name_function: function or None
254
+ if opening a set of files for writing, those files do not yet exist,
255
+ so we need to generate their names by formatting the urlpath for
256
+ each sequence number
257
+ num: int [1]
258
+ if writing mode, number of files we expect to create (passed to
259
+ name+function)
260
+ protocol: str or None
261
+ If given, overrides the protocol found in the URL.
262
+ newline: bytes or None
263
+ Used for line terminator in text mode. If None, uses system default;
264
+ if blank, uses no translation.
265
+ auto_mkdir: bool (True)
266
+ If in write mode, this will ensure the target directory exists before
267
+ writing, by calling ``fs.mkdirs(exist_ok=True)``.
268
+ expand: bool
269
+ **kwargs: dict
270
+ Extra options that make sense to a particular storage connection, e.g.
271
+ host, port, username, password, etc.
272
+
273
+ Examples
274
+ --------
275
+ >>> files = open_files('2015-*-*.csv') # doctest: +SKIP
276
+ >>> files = open_files(
277
+ ... 's3://bucket/2015-*-*.csv.gz', compression='gzip'
278
+ ... ) # doctest: +SKIP
279
+
280
+ Returns
281
+ -------
282
+ An ``OpenFiles`` instance, which is a list of ``OpenFile`` objects that can
283
+ be used as a single context
284
+
285
+ Notes
286
+ -----
287
+ For a full list of the available protocols and the implementations that
288
+ they map across to see the latest online documentation:
289
+
290
+ - For implementations built into ``fsspec`` see
291
+ https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations
292
+ - For implementations in separate packages see
293
+ https://filesystem-spec.readthedocs.io/en/latest/api.html#other-known-implementations
294
+ """
295
+ fs, fs_token, paths = get_fs_token_paths(
296
+ urlpath,
297
+ mode,
298
+ num=num,
299
+ name_function=name_function,
300
+ storage_options=kwargs,
301
+ protocol=protocol,
302
+ expand=expand,
303
+ )
304
+ if fs.protocol == "file":
305
+ fs.auto_mkdir = auto_mkdir
306
+ elif "r" not in mode and auto_mkdir:
307
+ parents = {fs._parent(path) for path in paths}
308
+ for parent in parents:
309
+ try:
310
+ fs.makedirs(parent, exist_ok=True)
311
+ except PermissionError:
312
+ pass
313
+ return OpenFiles(
314
+ [
315
+ OpenFile(
316
+ fs,
317
+ path,
318
+ mode=mode,
319
+ compression=compression,
320
+ encoding=encoding,
321
+ errors=errors,
322
+ newline=newline,
323
+ )
324
+ for path in paths
325
+ ],
326
+ mode=mode,
327
+ fs=fs,
328
+ )
329
+
330
+
331
+ def _un_chain(path, kwargs):
332
+ # Avoid a circular import
333
+ from fsspec.implementations.cached import CachingFileSystem
334
+
335
+ if "::" in path:
336
+ x = re.compile(".*[^a-z]+.*") # test for non protocol-like single word
337
+ bits = []
338
+ for p in path.split("::"):
339
+ if "://" in p or x.match(p):
340
+ bits.append(p)
341
+ else:
342
+ bits.append(p + "://")
343
+ else:
344
+ bits = [path]
345
+ # [[url, protocol, kwargs], ...]
346
+ out = []
347
+ previous_bit = None
348
+ kwargs = kwargs.copy()
349
+ for bit in reversed(bits):
350
+ protocol = kwargs.pop("protocol", None) or split_protocol(bit)[0] or "file"
351
+ cls = get_filesystem_class(protocol)
352
+ extra_kwargs = cls._get_kwargs_from_urls(bit)
353
+ kws = kwargs.pop(protocol, {})
354
+ if bit is bits[0]:
355
+ kws.update(kwargs)
356
+ kw = dict(
357
+ **{k: v for k, v in extra_kwargs.items() if k not in kws or v != kws[k]},
358
+ **kws,
359
+ )
360
+ bit = cls._strip_protocol(bit)
361
+ if "target_protocol" not in kw and issubclass(cls, CachingFileSystem):
362
+ bit = previous_bit
363
+ out.append((bit, protocol, kw))
364
+ previous_bit = bit
365
+ out.reverse()
366
+ return out
367
+
368
+
369
+ def url_to_fs(url, **kwargs):
370
+ """
371
+ Turn fully-qualified and potentially chained URL into filesystem instance
372
+
373
+ Parameters
374
+ ----------
375
+ url : str
376
+ The fsspec-compatible URL
377
+ **kwargs: dict
378
+ Extra options that make sense to a particular storage connection, e.g.
379
+ host, port, username, password, etc.
380
+
381
+ Returns
382
+ -------
383
+ filesystem : FileSystem
384
+ The new filesystem discovered from ``url`` and created with
385
+ ``**kwargs``.
386
+ urlpath : str
387
+ The file-systems-specific URL for ``url``.
388
+ """
389
+ url = stringify_path(url)
390
+ # non-FS arguments that appear in fsspec.open()
391
+ # inspect could keep this in sync with open()'s signature
392
+ known_kwargs = {
393
+ "compression",
394
+ "encoding",
395
+ "errors",
396
+ "expand",
397
+ "mode",
398
+ "name_function",
399
+ "newline",
400
+ "num",
401
+ }
402
+ kwargs = {k: v for k, v in kwargs.items() if k not in known_kwargs}
403
+ chain = _un_chain(url, kwargs)
404
+ inkwargs = {}
405
+ # Reverse iterate the chain, creating a nested target_* structure
406
+ for i, ch in enumerate(reversed(chain)):
407
+ urls, protocol, kw = ch
408
+ if i == len(chain) - 1:
409
+ inkwargs = dict(**kw, **inkwargs)
410
+ continue
411
+ inkwargs["target_options"] = dict(**kw, **inkwargs)
412
+ inkwargs["target_protocol"] = protocol
413
+ inkwargs["fo"] = urls
414
+ urlpath, protocol, _ = chain[0]
415
+ fs = filesystem(protocol, **inkwargs)
416
+ return fs, urlpath
417
+
418
+
419
+ DEFAULT_EXPAND = conf.get("open_expand", False)
420
+
421
+
422
+ def open(
423
+ urlpath,
424
+ mode="rb",
425
+ compression=None,
426
+ encoding="utf8",
427
+ errors=None,
428
+ protocol=None,
429
+ newline=None,
430
+ expand=None,
431
+ **kwargs,
432
+ ):
433
+ """Given a path or paths, return one ``OpenFile`` object.
434
+
435
+ Parameters
436
+ ----------
437
+ urlpath: string or list
438
+ Absolute or relative filepath. Prefix with a protocol like ``s3://``
439
+ to read from alternative filesystems. Should not include glob
440
+ character(s).
441
+ mode: 'rb', 'wt', etc.
442
+ compression: string or None
443
+ If given, open file using compression codec. Can either be a compression
444
+ name (a key in ``fsspec.compression.compr``) or "infer" to guess the
445
+ compression from the filename suffix.
446
+ encoding: str
447
+ For text mode only
448
+ errors: None or str
449
+ Passed to TextIOWrapper in text mode
450
+ protocol: str or None
451
+ If given, overrides the protocol found in the URL.
452
+ newline: bytes or None
453
+ Used for line terminator in text mode. If None, uses system default;
454
+ if blank, uses no translation.
455
+ expand: bool or None
456
+ Whether to regard file paths containing special glob characters as needing
457
+ expansion (finding the first match) or absolute. Setting False allows using
458
+ paths which do embed such characters. If None (default), this argument
459
+ takes its value from the DEFAULT_EXPAND module variable, which takes
460
+ its initial value from the "open_expand" config value at startup, which will
461
+ be False if not set.
462
+ **kwargs: dict
463
+ Extra options that make sense to a particular storage connection, e.g.
464
+ host, port, username, password, etc.
465
+
466
+ Examples
467
+ --------
468
+ >>> openfile = open('2015-01-01.csv') # doctest: +SKIP
469
+ >>> openfile = open(
470
+ ... 's3://bucket/2015-01-01.csv.gz', compression='gzip'
471
+ ... ) # doctest: +SKIP
472
+ >>> with openfile as f:
473
+ ... df = pd.read_csv(f) # doctest: +SKIP
474
+ ...
475
+
476
+ Returns
477
+ -------
478
+ ``OpenFile`` object.
479
+
480
+ Notes
481
+ -----
482
+ For a full list of the available protocols and the implementations that
483
+ they map across to see the latest online documentation:
484
+
485
+ - For implementations built into ``fsspec`` see
486
+ https://filesystem-spec.readthedocs.io/en/latest/api.html#built-in-implementations
487
+ - For implementations in separate packages see
488
+ https://filesystem-spec.readthedocs.io/en/latest/api.html#other-known-implementations
489
+ """
490
+ expand = DEFAULT_EXPAND if expand is None else expand
491
+ out = open_files(
492
+ urlpath=[urlpath],
493
+ mode=mode,
494
+ compression=compression,
495
+ encoding=encoding,
496
+ errors=errors,
497
+ protocol=protocol,
498
+ newline=newline,
499
+ expand=expand,
500
+ **kwargs,
501
+ )
502
+ if not out:
503
+ raise FileNotFoundError(urlpath)
504
+ return out[0]
505
+
506
+
507
+ def open_local(
508
+ url: str | list[str] | Path | list[Path],
509
+ mode: str = "rb",
510
+ **storage_options: dict,
511
+ ) -> str | list[str]:
512
+ """Open file(s) which can be resolved to local
513
+
514
+ For files which either are local, or get downloaded upon open
515
+ (e.g., by file caching)
516
+
517
+ Parameters
518
+ ----------
519
+ url: str or list(str)
520
+ mode: str
521
+ Must be read mode
522
+ storage_options:
523
+ passed on to FS for or used by open_files (e.g., compression)
524
+ """
525
+ if "r" not in mode:
526
+ raise ValueError("Can only ensure local files when reading")
527
+ of = open_files(url, mode=mode, **storage_options)
528
+ if not getattr(of[0].fs, "local_file", False):
529
+ raise ValueError(
530
+ "open_local can only be used on a filesystem which"
531
+ " has attribute local_file=True"
532
+ )
533
+ with of as files:
534
+ paths = [f.name for f in files]
535
+ if (isinstance(url, str) and not has_magic(url)) or isinstance(url, Path):
536
+ return paths[0]
537
+ return paths
538
+
539
+
540
+ def get_compression(urlpath, compression):
541
+ if compression == "infer":
542
+ compression = infer_compression(urlpath)
543
+ if compression is not None and compression not in compr:
544
+ raise ValueError(f"Compression type {compression} not supported")
545
+ return compression
546
+
547
+
548
+ def split_protocol(urlpath):
549
+ """Return protocol, path pair"""
550
+ urlpath = stringify_path(urlpath)
551
+ if "://" in urlpath:
552
+ protocol, path = urlpath.split("://", 1)
553
+ if len(protocol) > 1:
554
+ # excludes Windows paths
555
+ return protocol, path
556
+ if urlpath.startswith("data:"):
557
+ return urlpath.split(":", 1)
558
+ return None, urlpath
559
+
560
+
561
+ def strip_protocol(urlpath):
562
+ """Return only path part of full URL, according to appropriate backend"""
563
+ protocol, _ = split_protocol(urlpath)
564
+ cls = get_filesystem_class(protocol)
565
+ return cls._strip_protocol(urlpath)
566
+
567
+
568
+ def expand_paths_if_needed(paths, mode, num, fs, name_function):
569
+ """Expand paths if they have a ``*`` in them (write mode) or any of ``*?[]``
570
+ in them (read mode).
571
+
572
+ :param paths: list of paths
573
+ mode: str
574
+ Mode in which to open files.
575
+ num: int
576
+ If opening in writing mode, number of files we expect to create.
577
+ fs: filesystem object
578
+ name_function: callable
579
+ If opening in writing mode, this callable is used to generate path
580
+ names. Names are generated for each partition by
581
+ ``urlpath.replace('*', name_function(partition_index))``.
582
+ :return: list of paths
583
+ """
584
+ expanded_paths = []
585
+ paths = list(paths)
586
+
587
+ if "w" in mode: # read mode
588
+ if sum(1 for p in paths if "*" in p) > 1:
589
+ raise ValueError(
590
+ "When writing data, only one filename mask can be specified."
591
+ )
592
+ num = max(num, len(paths))
593
+
594
+ for curr_path in paths:
595
+ if "*" in curr_path:
596
+ # expand using name_function
597
+ expanded_paths.extend(_expand_paths(curr_path, name_function, num))
598
+ else:
599
+ expanded_paths.append(curr_path)
600
+ # if we generated more paths that asked for, trim the list
601
+ if len(expanded_paths) > num:
602
+ expanded_paths = expanded_paths[:num]
603
+
604
+ else: # read mode
605
+ for curr_path in paths:
606
+ if has_magic(curr_path):
607
+ # expand using glob
608
+ expanded_paths.extend(fs.glob(curr_path))
609
+ else:
610
+ expanded_paths.append(curr_path)
611
+
612
+ return expanded_paths
613
+
614
+
615
+ def get_fs_token_paths(
616
+ urlpath,
617
+ mode="rb",
618
+ num=1,
619
+ name_function=None,
620
+ storage_options=None,
621
+ protocol=None,
622
+ expand=True,
623
+ ):
624
+ """Filesystem, deterministic token, and paths from a urlpath and options.
625
+
626
+ Parameters
627
+ ----------
628
+ urlpath: string or iterable
629
+ Absolute or relative filepath, URL (may include protocols like
630
+ ``s3://``), or globstring pointing to data.
631
+ mode: str, optional
632
+ Mode in which to open files.
633
+ num: int, optional
634
+ If opening in writing mode, number of files we expect to create.
635
+ name_function: callable, optional
636
+ If opening in writing mode, this callable is used to generate path
637
+ names. Names are generated for each partition by
638
+ ``urlpath.replace('*', name_function(partition_index))``.
639
+ storage_options: dict, optional
640
+ Additional keywords to pass to the filesystem class.
641
+ protocol: str or None
642
+ To override the protocol specifier in the URL
643
+ expand: bool
644
+ Expand string paths for writing, assuming the path is a directory
645
+ """
646
+ if isinstance(urlpath, (list, tuple, set)):
647
+ if not urlpath:
648
+ raise ValueError("empty urlpath sequence")
649
+ urlpath0 = stringify_path(next(iter(urlpath)))
650
+ else:
651
+ urlpath0 = stringify_path(urlpath)
652
+ storage_options = storage_options or {}
653
+ if protocol:
654
+ storage_options["protocol"] = protocol
655
+ chain = _un_chain(urlpath0, storage_options or {})
656
+ inkwargs = {}
657
+ # Reverse iterate the chain, creating a nested target_* structure
658
+ for i, ch in enumerate(reversed(chain)):
659
+ urls, nested_protocol, kw = ch
660
+ if i == len(chain) - 1:
661
+ inkwargs = dict(**kw, **inkwargs)
662
+ continue
663
+ inkwargs["target_options"] = dict(**kw, **inkwargs)
664
+ inkwargs["target_protocol"] = nested_protocol
665
+ inkwargs["fo"] = urls
666
+ paths, protocol, _ = chain[0]
667
+ fs = filesystem(protocol, **inkwargs)
668
+ if isinstance(urlpath, (list, tuple, set)):
669
+ pchains = [
670
+ _un_chain(stringify_path(u), storage_options or {})[0] for u in urlpath
671
+ ]
672
+ if len({pc[1] for pc in pchains}) > 1:
673
+ raise ValueError("Protocol mismatch getting fs from %s", urlpath)
674
+ paths = [pc[0] for pc in pchains]
675
+ else:
676
+ paths = fs._strip_protocol(paths)
677
+ if isinstance(paths, (list, tuple, set)):
678
+ if expand:
679
+ paths = expand_paths_if_needed(paths, mode, num, fs, name_function)
680
+ elif not isinstance(paths, list):
681
+ paths = list(paths)
682
+ else:
683
+ if ("w" in mode or "x" in mode) and expand:
684
+ paths = _expand_paths(paths, name_function, num)
685
+ elif "*" in paths:
686
+ paths = [f for f in sorted(fs.glob(paths)) if not fs.isdir(f)]
687
+ else:
688
+ paths = [paths]
689
+
690
+ return fs, fs._fs_token, paths
691
+
692
+
693
+ def _expand_paths(path, name_function, num):
694
+ if isinstance(path, str):
695
+ if path.count("*") > 1:
696
+ raise ValueError("Output path spec must contain exactly one '*'.")
697
+ elif "*" not in path:
698
+ path = os.path.join(path, "*.part")
699
+
700
+ if name_function is None:
701
+ name_function = build_name_function(num - 1)
702
+
703
+ paths = [path.replace("*", name_function(i)) for i in range(num)]
704
+ if paths != sorted(paths):
705
+ logger.warning(
706
+ "In order to preserve order between partitions"
707
+ " paths created with ``name_function`` should "
708
+ "sort to partition order"
709
+ )
710
+ elif isinstance(path, (tuple, list)):
711
+ assert len(path) == num
712
+ paths = list(path)
713
+ else:
714
+ raise ValueError(
715
+ "Path should be either\n"
716
+ "1. A list of paths: ['foo.json', 'bar.json', ...]\n"
717
+ "2. A directory: 'foo/\n"
718
+ "3. A path with a '*' in it: 'foo.*.json'"
719
+ )
720
+ return paths
721
+
722
+
723
+ class PickleableTextIOWrapper(io.TextIOWrapper):
724
+ """TextIOWrapper cannot be pickled. This solves it.
725
+
726
+ Requires that ``buffer`` be pickleable, which all instances of
727
+ AbstractBufferedFile are.
728
+ """
729
+
730
+ def __init__(
731
+ self,
732
+ buffer,
733
+ encoding=None,
734
+ errors=None,
735
+ newline=None,
736
+ line_buffering=False,
737
+ write_through=False,
738
+ ):
739
+ self.args = buffer, encoding, errors, newline, line_buffering, write_through
740
+ super().__init__(*self.args)
741
+
742
+ def __reduce__(self):
743
+ return PickleableTextIOWrapper, self.args
lib/python3.12/site-packages/fsspec/dircache.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import time
2
+ from collections.abc import MutableMapping
3
+ from functools import lru_cache
4
+
5
+
6
+ class DirCache(MutableMapping):
7
+ """
8
+ Caching of directory listings, in a structure like::
9
+
10
+ {"path0": [
11
+ {"name": "path0/file0",
12
+ "size": 123,
13
+ "type": "file",
14
+ ...
15
+ },
16
+ {"name": "path0/file1",
17
+ },
18
+ ...
19
+ ],
20
+ "path1": [...]
21
+ }
22
+
23
+ Parameters to this class control listing expiry or indeed turn
24
+ caching off
25
+ """
26
+
27
+ def __init__(
28
+ self,
29
+ use_listings_cache=True,
30
+ listings_expiry_time=None,
31
+ max_paths=None,
32
+ **kwargs,
33
+ ):
34
+ """
35
+
36
+ Parameters
37
+ ----------
38
+ use_listings_cache: bool
39
+ If False, this cache never returns items, but always reports KeyError,
40
+ and setting items has no effect
41
+ listings_expiry_time: int or float (optional)
42
+ Time in seconds that a listing is considered valid. If None,
43
+ listings do not expire.
44
+ max_paths: int (optional)
45
+ The number of most recent listings that are considered valid; 'recent'
46
+ refers to when the entry was set.
47
+ """
48
+ self._cache = {}
49
+ self._times = {}
50
+ if max_paths:
51
+ self._q = lru_cache(max_paths + 1)(lambda key: self._cache.pop(key, None))
52
+ self.use_listings_cache = use_listings_cache
53
+ self.listings_expiry_time = listings_expiry_time
54
+ self.max_paths = max_paths
55
+
56
+ def __getitem__(self, item):
57
+ if self.listings_expiry_time is not None:
58
+ if self._times.get(item, 0) - time.time() < -self.listings_expiry_time:
59
+ del self._cache[item]
60
+ if self.max_paths:
61
+ self._q(item)
62
+ return self._cache[item] # maybe raises KeyError
63
+
64
+ def clear(self):
65
+ self._cache.clear()
66
+
67
+ def __len__(self):
68
+ return len(self._cache)
69
+
70
+ def __contains__(self, item):
71
+ try:
72
+ self[item]
73
+ return True
74
+ except KeyError:
75
+ return False
76
+
77
+ def __setitem__(self, key, value):
78
+ if not self.use_listings_cache:
79
+ return
80
+ if self.max_paths:
81
+ self._q(key)
82
+ self._cache[key] = value
83
+ if self.listings_expiry_time is not None:
84
+ self._times[key] = time.time()
85
+
86
+ def __delitem__(self, key):
87
+ del self._cache[key]
88
+
89
+ def __iter__(self):
90
+ entries = list(self._cache)
91
+
92
+ return (k for k in entries if k in self)
93
+
94
+ def __reduce__(self):
95
+ return (
96
+ DirCache,
97
+ (self.use_listings_cache, self.listings_expiry_time, self.max_paths),
98
+ )
lib/python3.12/site-packages/fsspec/exceptions.py ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ fsspec user-defined exception classes
3
+ """
4
+
5
+ import asyncio
6
+
7
+
8
+ class BlocksizeMismatchError(ValueError):
9
+ """
10
+ Raised when a cached file is opened with a different blocksize than it was
11
+ written with
12
+ """
13
+
14
+
15
+ class FSTimeoutError(asyncio.TimeoutError):
16
+ """
17
+ Raised when a fsspec function timed out occurs
18
+ """
lib/python3.12/site-packages/fsspec/fuse.py ADDED
@@ -0,0 +1,324 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import logging
3
+ import os
4
+ import stat
5
+ import threading
6
+ import time
7
+ from errno import EIO, ENOENT
8
+
9
+ from fuse import FUSE, FuseOSError, LoggingMixIn, Operations
10
+
11
+ from fsspec import __version__
12
+ from fsspec.core import url_to_fs
13
+
14
+ logger = logging.getLogger("fsspec.fuse")
15
+
16
+
17
+ class FUSEr(Operations):
18
+ def __init__(self, fs, path, ready_file=False):
19
+ self.fs = fs
20
+ self.cache = {}
21
+ self.root = path.rstrip("/") + "/"
22
+ self.counter = 0
23
+ logger.info("Starting FUSE at %s", path)
24
+ self._ready_file = ready_file
25
+
26
+ def getattr(self, path, fh=None):
27
+ logger.debug("getattr %s", path)
28
+ if self._ready_file and path in ["/.fuse_ready", ".fuse_ready"]:
29
+ return {"type": "file", "st_size": 5}
30
+
31
+ path = "".join([self.root, path.lstrip("/")]).rstrip("/")
32
+ try:
33
+ info = self.fs.info(path)
34
+ except FileNotFoundError as exc:
35
+ raise FuseOSError(ENOENT) from exc
36
+
37
+ data = {"st_uid": info.get("uid", 1000), "st_gid": info.get("gid", 1000)}
38
+ perm = info.get("mode", 0o777)
39
+
40
+ if info["type"] != "file":
41
+ data["st_mode"] = stat.S_IFDIR | perm
42
+ data["st_size"] = 0
43
+ data["st_blksize"] = 0
44
+ else:
45
+ data["st_mode"] = stat.S_IFREG | perm
46
+ data["st_size"] = info["size"]
47
+ data["st_blksize"] = 5 * 2**20
48
+ data["st_nlink"] = 1
49
+ data["st_atime"] = info["atime"] if "atime" in info else time.time()
50
+ data["st_ctime"] = info["ctime"] if "ctime" in info else time.time()
51
+ data["st_mtime"] = info["mtime"] if "mtime" in info else time.time()
52
+ return data
53
+
54
+ def readdir(self, path, fh):
55
+ logger.debug("readdir %s", path)
56
+ path = "".join([self.root, path.lstrip("/")])
57
+ files = self.fs.ls(path, False)
58
+ files = [os.path.basename(f.rstrip("/")) for f in files]
59
+ return [".", ".."] + files
60
+
61
+ def mkdir(self, path, mode):
62
+ path = "".join([self.root, path.lstrip("/")])
63
+ self.fs.mkdir(path)
64
+ return 0
65
+
66
+ def rmdir(self, path):
67
+ path = "".join([self.root, path.lstrip("/")])
68
+ self.fs.rmdir(path)
69
+ return 0
70
+
71
+ def read(self, path, size, offset, fh):
72
+ logger.debug("read %s", (path, size, offset))
73
+ if self._ready_file and path in ["/.fuse_ready", ".fuse_ready"]:
74
+ # status indicator
75
+ return b"ready"
76
+
77
+ f = self.cache[fh]
78
+ f.seek(offset)
79
+ out = f.read(size)
80
+ return out
81
+
82
+ def write(self, path, data, offset, fh):
83
+ logger.debug("write %s", (path, offset))
84
+ f = self.cache[fh]
85
+ f.seek(offset)
86
+ f.write(data)
87
+ return len(data)
88
+
89
+ def create(self, path, flags, fi=None):
90
+ logger.debug("create %s", (path, flags))
91
+ fn = "".join([self.root, path.lstrip("/")])
92
+ self.fs.touch(fn) # OS will want to get attributes immediately
93
+ f = self.fs.open(fn, "wb")
94
+ self.cache[self.counter] = f
95
+ self.counter += 1
96
+ return self.counter - 1
97
+
98
+ def open(self, path, flags):
99
+ logger.debug("open %s", (path, flags))
100
+ fn = "".join([self.root, path.lstrip("/")])
101
+ if flags % 2 == 0:
102
+ # read
103
+ mode = "rb"
104
+ else:
105
+ # write/create
106
+ mode = "wb"
107
+ self.cache[self.counter] = self.fs.open(fn, mode)
108
+ self.counter += 1
109
+ return self.counter - 1
110
+
111
+ def truncate(self, path, length, fh=None):
112
+ fn = "".join([self.root, path.lstrip("/")])
113
+ if length != 0:
114
+ raise NotImplementedError
115
+ # maybe should be no-op since open with write sets size to zero anyway
116
+ self.fs.touch(fn)
117
+
118
+ def unlink(self, path):
119
+ fn = "".join([self.root, path.lstrip("/")])
120
+ try:
121
+ self.fs.rm(fn, False)
122
+ except (OSError, FileNotFoundError) as exc:
123
+ raise FuseOSError(EIO) from exc
124
+
125
+ def release(self, path, fh):
126
+ try:
127
+ if fh in self.cache:
128
+ f = self.cache[fh]
129
+ f.close()
130
+ self.cache.pop(fh)
131
+ except Exception as e:
132
+ print(e)
133
+ return 0
134
+
135
+ def chmod(self, path, mode):
136
+ if hasattr(self.fs, "chmod"):
137
+ path = "".join([self.root, path.lstrip("/")])
138
+ return self.fs.chmod(path, mode)
139
+ raise NotImplementedError
140
+
141
+
142
+ def run(
143
+ fs,
144
+ path,
145
+ mount_point,
146
+ foreground=True,
147
+ threads=False,
148
+ ready_file=False,
149
+ ops_class=FUSEr,
150
+ ):
151
+ """Mount stuff in a local directory
152
+
153
+ This uses fusepy to make it appear as if a given path on an fsspec
154
+ instance is in fact resident within the local file-system.
155
+
156
+ This requires that fusepy by installed, and that FUSE be available on
157
+ the system (typically requiring a package to be installed with
158
+ apt, yum, brew, etc.).
159
+
160
+ Parameters
161
+ ----------
162
+ fs: file-system instance
163
+ From one of the compatible implementations
164
+ path: str
165
+ Location on that file-system to regard as the root directory to
166
+ mount. Note that you typically should include the terminating "/"
167
+ character.
168
+ mount_point: str
169
+ An empty directory on the local file-system where the contents of
170
+ the remote path will appear.
171
+ foreground: bool
172
+ Whether or not calling this function will block. Operation will
173
+ typically be more stable if True.
174
+ threads: bool
175
+ Whether or not to create threads when responding to file operations
176
+ within the mounter directory. Operation will typically be more
177
+ stable if False.
178
+ ready_file: bool
179
+ Whether the FUSE process is ready. The ``.fuse_ready`` file will
180
+ exist in the ``mount_point`` directory if True. Debugging purpose.
181
+ ops_class: FUSEr or Subclass of FUSEr
182
+ To override the default behavior of FUSEr. For Example, logging
183
+ to file.
184
+
185
+ """
186
+ func = lambda: FUSE(
187
+ ops_class(fs, path, ready_file=ready_file),
188
+ mount_point,
189
+ nothreads=not threads,
190
+ foreground=foreground,
191
+ )
192
+ if not foreground:
193
+ th = threading.Thread(target=func)
194
+ th.daemon = True
195
+ th.start()
196
+ return th
197
+ else: # pragma: no cover
198
+ try:
199
+ func()
200
+ except KeyboardInterrupt:
201
+ pass
202
+
203
+
204
+ def main(args):
205
+ """Mount filesystem from chained URL to MOUNT_POINT.
206
+
207
+ Examples:
208
+
209
+ python3 -m fsspec.fuse memory /usr/share /tmp/mem
210
+
211
+ python3 -m fsspec.fuse local /tmp/source /tmp/local \\
212
+ -l /tmp/fsspecfuse.log
213
+
214
+ You can also mount chained-URLs and use special settings:
215
+
216
+ python3 -m fsspec.fuse 'filecache::zip::file://data.zip' \\
217
+ / /tmp/zip \\
218
+ -o 'filecache-cache_storage=/tmp/simplecache'
219
+
220
+ You can specify the type of the setting by using `[int]` or `[bool]`,
221
+ (`true`, `yes`, `1` represents the Boolean value `True`):
222
+
223
+ python3 -m fsspec.fuse 'simplecache::ftp://ftp1.at.proftpd.org' \\
224
+ /historic/packages/RPMS /tmp/ftp \\
225
+ -o 'simplecache-cache_storage=/tmp/simplecache' \\
226
+ -o 'simplecache-check_files=false[bool]' \\
227
+ -o 'ftp-listings_expiry_time=60[int]' \\
228
+ -o 'ftp-username=anonymous' \\
229
+ -o 'ftp-password=xieyanbo'
230
+ """
231
+
232
+ class RawDescriptionArgumentParser(argparse.ArgumentParser):
233
+ def format_help(self):
234
+ usage = super().format_help()
235
+ parts = usage.split("\n\n")
236
+ parts[1] = self.description.rstrip()
237
+ return "\n\n".join(parts)
238
+
239
+ parser = RawDescriptionArgumentParser(prog="fsspec.fuse", description=main.__doc__)
240
+ parser.add_argument("--version", action="version", version=__version__)
241
+ parser.add_argument("url", type=str, help="fs url")
242
+ parser.add_argument("source_path", type=str, help="source directory in fs")
243
+ parser.add_argument("mount_point", type=str, help="local directory")
244
+ parser.add_argument(
245
+ "-o",
246
+ "--option",
247
+ action="append",
248
+ help="Any options of protocol included in the chained URL",
249
+ )
250
+ parser.add_argument(
251
+ "-l", "--log-file", type=str, help="Logging FUSE debug info (Default: '')"
252
+ )
253
+ parser.add_argument(
254
+ "-f",
255
+ "--foreground",
256
+ action="store_false",
257
+ help="Running in foreground or not (Default: False)",
258
+ )
259
+ parser.add_argument(
260
+ "-t",
261
+ "--threads",
262
+ action="store_false",
263
+ help="Running with threads support (Default: False)",
264
+ )
265
+ parser.add_argument(
266
+ "-r",
267
+ "--ready-file",
268
+ action="store_false",
269
+ help="The `.fuse_ready` file will exist after FUSE is ready. "
270
+ "(Debugging purpose, Default: False)",
271
+ )
272
+ args = parser.parse_args(args)
273
+
274
+ kwargs = {}
275
+ for item in args.option or []:
276
+ key, sep, value = item.partition("=")
277
+ if not sep:
278
+ parser.error(message=f"Wrong option: {item!r}")
279
+ val = value.lower()
280
+ if val.endswith("[int]"):
281
+ value = int(value[: -len("[int]")])
282
+ elif val.endswith("[bool]"):
283
+ value = val[: -len("[bool]")] in ["1", "yes", "true"]
284
+
285
+ if "-" in key:
286
+ fs_name, setting_name = key.split("-", 1)
287
+ if fs_name in kwargs:
288
+ kwargs[fs_name][setting_name] = value
289
+ else:
290
+ kwargs[fs_name] = {setting_name: value}
291
+ else:
292
+ kwargs[key] = value
293
+
294
+ if args.log_file:
295
+ logging.basicConfig(
296
+ level=logging.DEBUG,
297
+ filename=args.log_file,
298
+ format="%(asctime)s %(message)s",
299
+ )
300
+
301
+ class LoggingFUSEr(FUSEr, LoggingMixIn):
302
+ pass
303
+
304
+ fuser = LoggingFUSEr
305
+ else:
306
+ fuser = FUSEr
307
+
308
+ fs, url_path = url_to_fs(args.url, **kwargs)
309
+ logger.debug("Mounting %s to %s", url_path, str(args.mount_point))
310
+ run(
311
+ fs,
312
+ args.source_path,
313
+ args.mount_point,
314
+ foreground=args.foreground,
315
+ threads=args.threads,
316
+ ready_file=args.ready_file,
317
+ ops_class=fuser,
318
+ )
319
+
320
+
321
+ if __name__ == "__main__":
322
+ import sys
323
+
324
+ main(sys.argv[1:])
lib/python3.12/site-packages/fsspec/generic.py ADDED
@@ -0,0 +1,411 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import inspect
4
+ import logging
5
+ import os
6
+ import shutil
7
+ import uuid
8
+ from typing import Optional
9
+
10
+ from .asyn import AsyncFileSystem, _run_coros_in_chunks, sync_wrapper
11
+ from .callbacks import DEFAULT_CALLBACK
12
+ from .core import filesystem, get_filesystem_class, split_protocol, url_to_fs
13
+
14
+ _generic_fs = {}
15
+ logger = logging.getLogger("fsspec.generic")
16
+
17
+
18
+ def set_generic_fs(protocol, **storage_options):
19
+ _generic_fs[protocol] = filesystem(protocol, **storage_options)
20
+
21
+
22
+ default_method = "default"
23
+
24
+
25
+ def _resolve_fs(url, method=None, protocol=None, storage_options=None):
26
+ """Pick instance of backend FS"""
27
+ method = method or default_method
28
+ protocol = protocol or split_protocol(url)[0]
29
+ storage_options = storage_options or {}
30
+ if method == "default":
31
+ return filesystem(protocol)
32
+ if method == "generic":
33
+ return _generic_fs[protocol]
34
+ if method == "current":
35
+ cls = get_filesystem_class(protocol)
36
+ return cls.current()
37
+ if method == "options":
38
+ fs, _ = url_to_fs(url, **storage_options.get(protocol, {}))
39
+ return fs
40
+ raise ValueError(f"Unknown FS resolution method: {method}")
41
+
42
+
43
+ def rsync(
44
+ source,
45
+ destination,
46
+ delete_missing=False,
47
+ source_field="size",
48
+ dest_field="size",
49
+ update_cond="different",
50
+ inst_kwargs=None,
51
+ fs=None,
52
+ **kwargs,
53
+ ):
54
+ """Sync files between two directory trees
55
+
56
+ (experimental)
57
+
58
+ Parameters
59
+ ----------
60
+ source: str
61
+ Root of the directory tree to take files from. This must be a directory, but
62
+ do not include any terminating "/" character
63
+ destination: str
64
+ Root path to copy into. The contents of this location should be
65
+ identical to the contents of ``source`` when done. This will be made a
66
+ directory, and the terminal "/" should not be included.
67
+ delete_missing: bool
68
+ If there are paths in the destination that don't exist in the
69
+ source and this is True, delete them. Otherwise, leave them alone.
70
+ source_field: str | callable
71
+ If ``update_field`` is "different", this is the key in the info
72
+ of source files to consider for difference. Maybe a function of the
73
+ info dict.
74
+ dest_field: str | callable
75
+ If ``update_field`` is "different", this is the key in the info
76
+ of destination files to consider for difference. May be a function of
77
+ the info dict.
78
+ update_cond: "different"|"always"|"never"
79
+ If "always", every file is copied, regardless of whether it exists in
80
+ the destination. If "never", files that exist in the destination are
81
+ not copied again. If "different" (default), only copy if the info
82
+ fields given by ``source_field`` and ``dest_field`` (usually "size")
83
+ are different. Other comparisons may be added in the future.
84
+ inst_kwargs: dict|None
85
+ If ``fs`` is None, use this set of keyword arguments to make a
86
+ GenericFileSystem instance
87
+ fs: GenericFileSystem|None
88
+ Instance to use if explicitly given. The instance defines how to
89
+ to make downstream file system instances from paths.
90
+
91
+ Returns
92
+ -------
93
+ dict of the copy operations that were performed, {source: destination}
94
+ """
95
+ fs = fs or GenericFileSystem(**(inst_kwargs or {}))
96
+ source = fs._strip_protocol(source)
97
+ destination = fs._strip_protocol(destination)
98
+ allfiles = fs.find(source, withdirs=True, detail=True)
99
+ if not fs.isdir(source):
100
+ raise ValueError("Can only rsync on a directory")
101
+ otherfiles = fs.find(destination, withdirs=True, detail=True)
102
+ dirs = [
103
+ a
104
+ for a, v in allfiles.items()
105
+ if v["type"] == "directory" and a.replace(source, destination) not in otherfiles
106
+ ]
107
+ logger.debug(f"{len(dirs)} directories to create")
108
+ if dirs:
109
+ fs.make_many_dirs(
110
+ [dirn.replace(source, destination) for dirn in dirs], exist_ok=True
111
+ )
112
+ allfiles = {a: v for a, v in allfiles.items() if v["type"] == "file"}
113
+ logger.debug(f"{len(allfiles)} files to consider for copy")
114
+ to_delete = [
115
+ o
116
+ for o, v in otherfiles.items()
117
+ if o.replace(destination, source) not in allfiles and v["type"] == "file"
118
+ ]
119
+ for k, v in allfiles.copy().items():
120
+ otherfile = k.replace(source, destination)
121
+ if otherfile in otherfiles:
122
+ if update_cond == "always":
123
+ allfiles[k] = otherfile
124
+ elif update_cond == "different":
125
+ inf1 = source_field(v) if callable(source_field) else v[source_field]
126
+ v2 = otherfiles[otherfile]
127
+ inf2 = dest_field(v2) if callable(dest_field) else v2[dest_field]
128
+ if inf1 != inf2:
129
+ # details mismatch, make copy
130
+ allfiles[k] = otherfile
131
+ else:
132
+ # details match, don't copy
133
+ allfiles.pop(k)
134
+ else:
135
+ # file not in target yet
136
+ allfiles[k] = otherfile
137
+ logger.debug(f"{len(allfiles)} files to copy")
138
+ if allfiles:
139
+ source_files, target_files = zip(*allfiles.items())
140
+ fs.cp(source_files, target_files, **kwargs)
141
+ logger.debug(f"{len(to_delete)} files to delete")
142
+ if delete_missing and to_delete:
143
+ fs.rm(to_delete)
144
+ return allfiles
145
+
146
+
147
+ class GenericFileSystem(AsyncFileSystem):
148
+ """Wrapper over all other FS types
149
+
150
+ <experimental!>
151
+
152
+ This implementation is a single unified interface to be able to run FS operations
153
+ over generic URLs, and dispatch to the specific implementations using the URL
154
+ protocol prefix.
155
+
156
+ Note: instances of this FS are always async, even if you never use it with any async
157
+ backend.
158
+ """
159
+
160
+ protocol = "generic" # there is no real reason to ever use a protocol with this FS
161
+
162
+ def __init__(self, default_method="default", **kwargs):
163
+ """
164
+
165
+ Parameters
166
+ ----------
167
+ default_method: str (optional)
168
+ Defines how to configure backend FS instances. Options are:
169
+ - "default": instantiate like FSClass(), with no
170
+ extra arguments; this is the default instance of that FS, and can be
171
+ configured via the config system
172
+ - "generic": takes instances from the `_generic_fs` dict in this module,
173
+ which you must populate before use. Keys are by protocol
174
+ - "current": takes the most recently instantiated version of each FS
175
+ """
176
+ self.method = default_method
177
+ super().__init__(**kwargs)
178
+
179
+ def _parent(self, path):
180
+ fs = _resolve_fs(path, self.method)
181
+ return fs.unstrip_protocol(fs._parent(path))
182
+
183
+ def _strip_protocol(self, path):
184
+ # normalization only
185
+ fs = _resolve_fs(path, self.method)
186
+ return fs.unstrip_protocol(fs._strip_protocol(path))
187
+
188
+ async def _find(self, path, maxdepth=None, withdirs=False, detail=False, **kwargs):
189
+ fs = _resolve_fs(path, self.method)
190
+ if fs.async_impl:
191
+ out = await fs._find(
192
+ path, maxdepth=maxdepth, withdirs=withdirs, detail=True, **kwargs
193
+ )
194
+ else:
195
+ out = fs.find(
196
+ path, maxdepth=maxdepth, withdirs=withdirs, detail=True, **kwargs
197
+ )
198
+ result = {}
199
+ for k, v in out.items():
200
+ v = v.copy() # don't corrupt target FS dircache
201
+ name = fs.unstrip_protocol(k)
202
+ v["name"] = name
203
+ result[name] = v
204
+ if detail:
205
+ return result
206
+ return list(result)
207
+
208
+ async def _info(self, url, **kwargs):
209
+ fs = _resolve_fs(url, self.method)
210
+ if fs.async_impl:
211
+ out = await fs._info(url, **kwargs)
212
+ else:
213
+ out = fs.info(url, **kwargs)
214
+ out = out.copy() # don't edit originals
215
+ out["name"] = fs.unstrip_protocol(out["name"])
216
+ return out
217
+
218
+ async def _ls(
219
+ self,
220
+ url,
221
+ detail=True,
222
+ **kwargs,
223
+ ):
224
+ fs = _resolve_fs(url, self.method)
225
+ if fs.async_impl:
226
+ out = await fs._ls(url, detail=True, **kwargs)
227
+ else:
228
+ out = fs.ls(url, detail=True, **kwargs)
229
+ out = [o.copy() for o in out] # don't edit originals
230
+ for o in out:
231
+ o["name"] = fs.unstrip_protocol(o["name"])
232
+ if detail:
233
+ return out
234
+ else:
235
+ return [o["name"] for o in out]
236
+
237
+ async def _cat_file(
238
+ self,
239
+ url,
240
+ **kwargs,
241
+ ):
242
+ fs = _resolve_fs(url, self.method)
243
+ if fs.async_impl:
244
+ return await fs._cat_file(url, **kwargs)
245
+ else:
246
+ return fs.cat_file(url, **kwargs)
247
+
248
+ async def _pipe_file(
249
+ self,
250
+ path,
251
+ value,
252
+ **kwargs,
253
+ ):
254
+ fs = _resolve_fs(path, self.method)
255
+ if fs.async_impl:
256
+ return await fs._pipe_file(path, value, **kwargs)
257
+ else:
258
+ return fs.pipe_file(path, value, **kwargs)
259
+
260
+ async def _rm(self, url, **kwargs):
261
+ urls = url
262
+ if isinstance(urls, str):
263
+ urls = [urls]
264
+ fs = _resolve_fs(urls[0], self.method)
265
+ if fs.async_impl:
266
+ await fs._rm(urls, **kwargs)
267
+ else:
268
+ fs.rm(url, **kwargs)
269
+
270
+ async def _makedirs(self, path, exist_ok=False):
271
+ logger.debug("Make dir %s", path)
272
+ fs = _resolve_fs(path, self.method)
273
+ if fs.async_impl:
274
+ await fs._makedirs(path, exist_ok=exist_ok)
275
+ else:
276
+ fs.makedirs(path, exist_ok=exist_ok)
277
+
278
+ def rsync(self, source, destination, **kwargs):
279
+ """Sync files between two directory trees
280
+
281
+ See `func:rsync` for more details.
282
+ """
283
+ rsync(source, destination, fs=self, **kwargs)
284
+
285
+ async def _cp_file(
286
+ self,
287
+ url,
288
+ url2,
289
+ blocksize=2**20,
290
+ callback=DEFAULT_CALLBACK,
291
+ **kwargs,
292
+ ):
293
+ fs = _resolve_fs(url, self.method)
294
+ fs2 = _resolve_fs(url2, self.method)
295
+ if fs is fs2:
296
+ # pure remote
297
+ if fs.async_impl:
298
+ return await fs._cp_file(url, url2, **kwargs)
299
+ else:
300
+ return fs.cp_file(url, url2, **kwargs)
301
+ kw = {"blocksize": 0, "cache_type": "none"}
302
+ try:
303
+ f1 = (
304
+ await fs.open_async(url, "rb")
305
+ if hasattr(fs, "open_async")
306
+ else fs.open(url, "rb", **kw)
307
+ )
308
+ callback.set_size(await maybe_await(f1.size))
309
+ f2 = (
310
+ await fs2.open_async(url2, "wb")
311
+ if hasattr(fs2, "open_async")
312
+ else fs2.open(url2, "wb", **kw)
313
+ )
314
+ while f1.size is None or f2.tell() < f1.size:
315
+ data = await maybe_await(f1.read(blocksize))
316
+ if f1.size is None and not data:
317
+ break
318
+ await maybe_await(f2.write(data))
319
+ callback.absolute_update(f2.tell())
320
+ finally:
321
+ try:
322
+ await maybe_await(f2.close())
323
+ await maybe_await(f1.close())
324
+ except NameError:
325
+ # fail while opening f1 or f2
326
+ pass
327
+
328
+ async def _make_many_dirs(self, urls, exist_ok=True):
329
+ fs = _resolve_fs(urls[0], self.method)
330
+ if fs.async_impl:
331
+ coros = [fs._makedirs(u, exist_ok=exist_ok) for u in urls]
332
+ await _run_coros_in_chunks(coros)
333
+ else:
334
+ for u in urls:
335
+ fs.makedirs(u, exist_ok=exist_ok)
336
+
337
+ make_many_dirs = sync_wrapper(_make_many_dirs)
338
+
339
+ async def _copy(
340
+ self,
341
+ path1: list[str],
342
+ path2: list[str],
343
+ recursive: bool = False,
344
+ on_error: str = "ignore",
345
+ maxdepth: Optional[int] = None,
346
+ batch_size: Optional[int] = None,
347
+ tempdir: Optional[str] = None,
348
+ **kwargs,
349
+ ):
350
+ if recursive:
351
+ raise NotImplementedError
352
+ fs = _resolve_fs(path1[0], self.method)
353
+ fs2 = _resolve_fs(path2[0], self.method)
354
+ # not expanding paths atm., assume call is from rsync()
355
+ if fs is fs2:
356
+ # pure remote
357
+ if fs.async_impl:
358
+ return await fs._copy(path1, path2, **kwargs)
359
+ else:
360
+ return fs.copy(path1, path2, **kwargs)
361
+ await copy_file_op(
362
+ fs, path1, fs2, path2, tempdir, batch_size, on_error=on_error
363
+ )
364
+
365
+
366
+ async def copy_file_op(
367
+ fs1, url1, fs2, url2, tempdir=None, batch_size=20, on_error="ignore"
368
+ ):
369
+ import tempfile
370
+
371
+ tempdir = tempdir or tempfile.mkdtemp()
372
+ try:
373
+ coros = [
374
+ _copy_file_op(
375
+ fs1,
376
+ u1,
377
+ fs2,
378
+ u2,
379
+ os.path.join(tempdir, uuid.uuid4().hex),
380
+ on_error=on_error,
381
+ )
382
+ for u1, u2 in zip(url1, url2)
383
+ ]
384
+ await _run_coros_in_chunks(coros, batch_size=batch_size)
385
+ finally:
386
+ shutil.rmtree(tempdir)
387
+
388
+
389
+ async def _copy_file_op(fs1, url1, fs2, url2, local, on_error="ignore"):
390
+ ex = () if on_error == "raise" else Exception
391
+ logger.debug("Copy %s -> %s", url1, url2)
392
+ try:
393
+ if fs1.async_impl:
394
+ await fs1._get_file(url1, local)
395
+ else:
396
+ fs1.get_file(url1, local)
397
+ if fs2.async_impl:
398
+ await fs2._put_file(local, url2)
399
+ else:
400
+ fs2.put_file(local, url2)
401
+ os.unlink(local)
402
+ logger.debug("Copy %s -> %s; done", url1, url2)
403
+ except ex as e:
404
+ logger.debug("ignoring cp exception for %s: %s", url1, e)
405
+
406
+
407
+ async def maybe_await(cor):
408
+ if inspect.iscoroutine(cor):
409
+ return await cor
410
+ else:
411
+ return cor
lib/python3.12/site-packages/fsspec/gui.py ADDED
@@ -0,0 +1,416 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ast
2
+ import contextlib
3
+ import logging
4
+ import os
5
+ import re
6
+ from typing import ClassVar, Sequence
7
+
8
+ import panel as pn
9
+
10
+ from .core import OpenFile, get_filesystem_class, split_protocol
11
+ from .registry import known_implementations
12
+
13
+ pn.extension()
14
+ logger = logging.getLogger("fsspec.gui")
15
+
16
+
17
+ class SigSlot:
18
+ """Signal-slot mixin, for Panel event passing
19
+
20
+ Include this class in a widget manager's superclasses to be able to
21
+ register events and callbacks on Panel widgets managed by that class.
22
+
23
+ The method ``_register`` should be called as widgets are added, and external
24
+ code should call ``connect`` to associate callbacks.
25
+
26
+ By default, all signals emit a DEBUG logging statement.
27
+ """
28
+
29
+ # names of signals that this class may emit each of which must be
30
+ # set by _register for any new instance
31
+ signals: ClassVar[Sequence[str]] = []
32
+ # names of actions that this class may respond to
33
+ slots: ClassVar[Sequence[str]] = []
34
+
35
+ # each of which must be a method name
36
+
37
+ def __init__(self):
38
+ self._ignoring_events = False
39
+ self._sigs = {}
40
+ self._map = {}
41
+ self._setup()
42
+
43
+ def _setup(self):
44
+ """Create GUI elements and register signals"""
45
+ self.panel = pn.pane.PaneBase()
46
+ # no signals to set up in the base class
47
+
48
+ def _register(
49
+ self, widget, name, thing="value", log_level=logging.DEBUG, auto=False
50
+ ):
51
+ """Watch the given attribute of a widget and assign it a named event
52
+
53
+ This is normally called at the time a widget is instantiated, in the
54
+ class which owns it.
55
+
56
+ Parameters
57
+ ----------
58
+ widget : pn.layout.Panel or None
59
+ Widget to watch. If None, an anonymous signal not associated with
60
+ any widget.
61
+ name : str
62
+ Name of this event
63
+ thing : str
64
+ Attribute of the given widget to watch
65
+ log_level : int
66
+ When the signal is triggered, a logging event of the given level
67
+ will be fired in the dfviz logger.
68
+ auto : bool
69
+ If True, automatically connects with a method in this class of the
70
+ same name.
71
+ """
72
+ if name not in self.signals:
73
+ raise ValueError(f"Attempt to assign an undeclared signal: {name}")
74
+ self._sigs[name] = {
75
+ "widget": widget,
76
+ "callbacks": [],
77
+ "thing": thing,
78
+ "log": log_level,
79
+ }
80
+ wn = "-".join(
81
+ [
82
+ getattr(widget, "name", str(widget)) if widget is not None else "none",
83
+ thing,
84
+ ]
85
+ )
86
+ self._map[wn] = name
87
+ if widget is not None:
88
+ widget.param.watch(self._signal, thing, onlychanged=True)
89
+ if auto and hasattr(self, name):
90
+ self.connect(name, getattr(self, name))
91
+
92
+ def _repr_mimebundle_(self, *args, **kwargs):
93
+ """Display in a notebook or a server"""
94
+ try:
95
+ return self.panel._repr_mimebundle_(*args, **kwargs)
96
+ except (ValueError, AttributeError) as exc:
97
+ raise NotImplementedError(
98
+ "Panel does not seem to be set up properly"
99
+ ) from exc
100
+
101
+ def connect(self, signal, slot):
102
+ """Associate call back with given event
103
+
104
+ The callback must be a function which takes the "new" value of the
105
+ watched attribute as the only parameter. If the callback return False,
106
+ this cancels any further processing of the given event.
107
+
108
+ Alternatively, the callback can be a string, in which case it means
109
+ emitting the correspondingly-named event (i.e., connect to self)
110
+ """
111
+ self._sigs[signal]["callbacks"].append(slot)
112
+
113
+ def _signal(self, event):
114
+ """This is called by a an action on a widget
115
+
116
+ Within an self.ignore_events context, nothing happens.
117
+
118
+ Tests can execute this method by directly changing the values of
119
+ widget components.
120
+ """
121
+ if not self._ignoring_events:
122
+ wn = "-".join([event.obj.name, event.name])
123
+ if wn in self._map and self._map[wn] in self._sigs:
124
+ self._emit(self._map[wn], event.new)
125
+
126
+ @contextlib.contextmanager
127
+ def ignore_events(self):
128
+ """Temporarily turn off events processing in this instance
129
+
130
+ (does not propagate to children)
131
+ """
132
+ self._ignoring_events = True
133
+ try:
134
+ yield
135
+ finally:
136
+ self._ignoring_events = False
137
+
138
+ def _emit(self, sig, value=None):
139
+ """An event happened, call its callbacks
140
+
141
+ This method can be used in tests to simulate message passing without
142
+ directly changing visual elements.
143
+
144
+ Calling of callbacks will halt whenever one returns False.
145
+ """
146
+ logger.log(self._sigs[sig]["log"], f"{sig}: {value}")
147
+ for callback in self._sigs[sig]["callbacks"]:
148
+ if isinstance(callback, str):
149
+ self._emit(callback)
150
+ else:
151
+ try:
152
+ # running callbacks should not break the interface
153
+ ret = callback(value)
154
+ if ret is False:
155
+ break
156
+ except Exception as e:
157
+ logger.exception(
158
+ "Exception (%s) while executing callback for signal: %s",
159
+ e,
160
+ sig,
161
+ )
162
+
163
+ def show(self, threads=False):
164
+ """Open a new browser tab and display this instance's interface"""
165
+ self.panel.show(threads=threads, verbose=False)
166
+ return self
167
+
168
+
169
+ class SingleSelect(SigSlot):
170
+ """A multiselect which only allows you to select one item for an event"""
171
+
172
+ signals = ["_selected", "selected"] # the first is internal
173
+ slots = ["set_options", "set_selection", "add", "clear", "select"]
174
+
175
+ def __init__(self, **kwargs):
176
+ self.kwargs = kwargs
177
+ super().__init__()
178
+
179
+ def _setup(self):
180
+ self.panel = pn.widgets.MultiSelect(**self.kwargs)
181
+ self._register(self.panel, "_selected", "value")
182
+ self._register(None, "selected")
183
+ self.connect("_selected", self.select_one)
184
+
185
+ def _signal(self, *args, **kwargs):
186
+ super()._signal(*args, **kwargs)
187
+
188
+ def select_one(self, *_):
189
+ with self.ignore_events():
190
+ val = [self.panel.value[-1]] if self.panel.value else []
191
+ self.panel.value = val
192
+ self._emit("selected", self.panel.value)
193
+
194
+ def set_options(self, options):
195
+ self.panel.options = options
196
+
197
+ def clear(self):
198
+ self.panel.options = []
199
+
200
+ @property
201
+ def value(self):
202
+ return self.panel.value
203
+
204
+ def set_selection(self, selection):
205
+ self.panel.value = [selection]
206
+
207
+
208
+ class FileSelector(SigSlot):
209
+ """Panel-based graphical file selector widget
210
+
211
+ Instances of this widget are interactive and can be displayed in jupyter by having
212
+ them as the output of a cell, or in a separate browser tab using ``.show()``.
213
+ """
214
+
215
+ signals = [
216
+ "protocol_changed",
217
+ "selection_changed",
218
+ "directory_entered",
219
+ "home_clicked",
220
+ "up_clicked",
221
+ "go_clicked",
222
+ "filters_changed",
223
+ ]
224
+ slots = ["set_filters", "go_home"]
225
+
226
+ def __init__(self, url=None, filters=None, ignore=None, kwargs=None):
227
+ """
228
+
229
+ Parameters
230
+ ----------
231
+ url : str (optional)
232
+ Initial value of the URL to populate the dialog; should include protocol
233
+ filters : list(str) (optional)
234
+ File endings to include in the listings. If not included, all files are
235
+ allowed. Does not affect directories.
236
+ If given, the endings will appear as checkboxes in the interface
237
+ ignore : list(str) (optional)
238
+ Regex(s) of file basename patterns to ignore, e.g., "\\." for typical
239
+ hidden files on posix
240
+ kwargs : dict (optional)
241
+ To pass to file system instance
242
+ """
243
+ if url:
244
+ self.init_protocol, url = split_protocol(url)
245
+ else:
246
+ self.init_protocol, url = "file", os.getcwd()
247
+ self.init_url = url
248
+ self.init_kwargs = (kwargs if isinstance(kwargs, str) else str(kwargs)) or "{}"
249
+ self.filters = filters
250
+ self.ignore = [re.compile(i) for i in ignore or []]
251
+ self._fs = None
252
+ super().__init__()
253
+
254
+ def _setup(self):
255
+ self.url = pn.widgets.TextInput(
256
+ name="url",
257
+ value=self.init_url,
258
+ align="end",
259
+ sizing_mode="stretch_width",
260
+ width_policy="max",
261
+ )
262
+ self.protocol = pn.widgets.Select(
263
+ options=sorted(known_implementations),
264
+ value=self.init_protocol,
265
+ name="protocol",
266
+ align="center",
267
+ )
268
+ self.kwargs = pn.widgets.TextInput(
269
+ name="kwargs", value=self.init_kwargs, align="center"
270
+ )
271
+ self.go = pn.widgets.Button(name="⇨", align="end", width=45)
272
+ self.main = SingleSelect(size=10)
273
+ self.home = pn.widgets.Button(name="🏠", width=40, height=30, align="end")
274
+ self.up = pn.widgets.Button(name="‹", width=30, height=30, align="end")
275
+
276
+ self._register(self.protocol, "protocol_changed", auto=True)
277
+ self._register(self.go, "go_clicked", "clicks", auto=True)
278
+ self._register(self.up, "up_clicked", "clicks", auto=True)
279
+ self._register(self.home, "home_clicked", "clicks", auto=True)
280
+ self._register(None, "selection_changed")
281
+ self.main.connect("selected", self.selection_changed)
282
+ self._register(None, "directory_entered")
283
+ self.prev_protocol = self.protocol.value
284
+ self.prev_kwargs = self.storage_options
285
+
286
+ self.filter_sel = pn.widgets.CheckBoxGroup(
287
+ value=[], options=[], inline=False, align="end", width_policy="min"
288
+ )
289
+ self._register(self.filter_sel, "filters_changed", auto=True)
290
+
291
+ self.panel = pn.Column(
292
+ pn.Row(self.protocol, self.kwargs),
293
+ pn.Row(self.home, self.up, self.url, self.go, self.filter_sel),
294
+ self.main.panel,
295
+ )
296
+ self.set_filters(self.filters)
297
+ self.go_clicked()
298
+
299
+ def set_filters(self, filters=None):
300
+ self.filters = filters
301
+ if filters:
302
+ self.filter_sel.options = filters
303
+ self.filter_sel.value = filters
304
+ else:
305
+ self.filter_sel.options = []
306
+ self.filter_sel.value = []
307
+
308
+ @property
309
+ def storage_options(self):
310
+ """Value of the kwargs box as a dictionary"""
311
+ return ast.literal_eval(self.kwargs.value) or {}
312
+
313
+ @property
314
+ def fs(self):
315
+ """Current filesystem instance"""
316
+ if self._fs is None:
317
+ cls = get_filesystem_class(self.protocol.value)
318
+ self._fs = cls(**self.storage_options)
319
+ return self._fs
320
+
321
+ @property
322
+ def urlpath(self):
323
+ """URL of currently selected item"""
324
+ return (
325
+ (f"{self.protocol.value}://{self.main.value[0]}")
326
+ if self.main.value
327
+ else None
328
+ )
329
+
330
+ def open_file(self, mode="rb", compression=None, encoding=None):
331
+ """Create OpenFile instance for the currently selected item
332
+
333
+ For example, in a notebook you might do something like
334
+
335
+ .. code-block::
336
+
337
+ [ ]: sel = FileSelector(); sel
338
+
339
+ # user selects their file
340
+
341
+ [ ]: with sel.open_file('rb') as f:
342
+ ... out = f.read()
343
+
344
+ Parameters
345
+ ----------
346
+ mode: str (optional)
347
+ Open mode for the file.
348
+ compression: str (optional)
349
+ The interact with the file as compressed. Set to 'infer' to guess
350
+ compression from the file ending
351
+ encoding: str (optional)
352
+ If using text mode, use this encoding; defaults to UTF8.
353
+ """
354
+ if self.urlpath is None:
355
+ raise ValueError("No file selected")
356
+ return OpenFile(self.fs, self.urlpath, mode, compression, encoding)
357
+
358
+ def filters_changed(self, values):
359
+ self.filters = values
360
+ self.go_clicked()
361
+
362
+ def selection_changed(self, *_):
363
+ if self.urlpath is None:
364
+ return
365
+ if self.fs.isdir(self.urlpath):
366
+ self.url.value = self.fs._strip_protocol(self.urlpath)
367
+ self.go_clicked()
368
+
369
+ def go_clicked(self, *_):
370
+ if (
371
+ self.prev_protocol != self.protocol.value
372
+ or self.prev_kwargs != self.storage_options
373
+ ):
374
+ self._fs = None # causes fs to be recreated
375
+ self.prev_protocol = self.protocol.value
376
+ self.prev_kwargs = self.storage_options
377
+ listing = sorted(
378
+ self.fs.ls(self.url.value, detail=True), key=lambda x: x["name"]
379
+ )
380
+ listing = [
381
+ l
382
+ for l in listing
383
+ if not any(i.match(l["name"].rsplit("/", 1)[-1]) for i in self.ignore)
384
+ ]
385
+ folders = {
386
+ "📁 " + o["name"].rsplit("/", 1)[-1]: o["name"]
387
+ for o in listing
388
+ if o["type"] == "directory"
389
+ }
390
+ files = {
391
+ "📄 " + o["name"].rsplit("/", 1)[-1]: o["name"]
392
+ for o in listing
393
+ if o["type"] == "file"
394
+ }
395
+ if self.filters:
396
+ files = {
397
+ k: v
398
+ for k, v in files.items()
399
+ if any(v.endswith(ext) for ext in self.filters)
400
+ }
401
+ self.main.set_options(dict(**folders, **files))
402
+
403
+ def protocol_changed(self, *_):
404
+ self._fs = None
405
+ self.main.options = []
406
+ self.url.value = ""
407
+
408
+ def home_clicked(self, *_):
409
+ self.protocol.value = self.init_protocol
410
+ self.kwargs.value = self.init_kwargs
411
+ self.url.value = self.init_url
412
+ self.go_clicked()
413
+
414
+ def up_clicked(self, *_):
415
+ self.url.value = self.fs._parent(self.url.value)
416
+ self.go_clicked()
lib/python3.12/site-packages/fsspec/implementations/__init__.py ADDED
File without changes
lib/python3.12/site-packages/fsspec/implementations/__pycache__/__init__.cpython-312.pyc ADDED
Binary file (201 Bytes). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/arrow.cpython-312.pyc ADDED
Binary file (13.5 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/asyn_wrapper.cpython-312.pyc ADDED
Binary file (4.81 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/cache_mapper.cpython-312.pyc ADDED
Binary file (4.01 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/cache_metadata.cpython-312.pyc ADDED
Binary file (11.1 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/cached.cpython-312.pyc ADDED
Binary file (43.4 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/dask.cpython-312.pyc ADDED
Binary file (7.11 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/data.cpython-312.pyc ADDED
Binary file (3.05 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/dbfs.cpython-312.pyc ADDED
Binary file (18.2 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/dirfs.cpython-312.pyc ADDED
Binary file (24.9 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/ftp.cpython-312.pyc ADDED
Binary file (17.7 kB). View file
 
lib/python3.12/site-packages/fsspec/implementations/__pycache__/git.cpython-312.pyc ADDED
Binary file (5.94 kB). View file