notebookutils-py 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- notebookutils/__init__.py +19 -0
- notebookutils/fs/__init__.py +359 -0
- notebookutils/fs/_client.py +69 -0
- notebookutils/fs/_fileinfo.py +47 -0
- notebookutils/fs/_mount.py +268 -0
- notebookutils/fs/_mount_azure.py +188 -0
- notebookutils/fs/_mount_gcs.py +51 -0
- notebookutils/fs/_mount_s3.py +70 -0
- notebookutils/fs/_registry.py +86 -0
- notebookutils_py-0.1.0.dist-info/METADATA +359 -0
- notebookutils_py-0.1.0.dist-info/RECORD +13 -0
- notebookutils_py-0.1.0.dist-info/WHEEL +4 -0
- notebookutils_py-0.1.0.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"""notebookutils: platform-agnostic notebook utilities.
|
|
2
|
+
|
|
3
|
+
Provides :mod:`notebookutils.fs`, a Fabric/Synapse-compatible file system
|
|
4
|
+
utility module backed by ``fsspec`` (memory-space operations) and FUSE
|
|
5
|
+
binaries (POSIX mounting), usable on any local, VM, or Spark/K8s environment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import sys as _sys
|
|
9
|
+
|
|
10
|
+
from notebookutils import fs
|
|
11
|
+
|
|
12
|
+
__all__ = ["fs"]
|
|
13
|
+
__version__ = "0.1.0"
|
|
14
|
+
|
|
15
|
+
# Best-effort compatibility shim: after `import notebookutils`, legacy code
|
|
16
|
+
# using `import mssparkutils` / `mssparkutils.fs` also works within the same
|
|
17
|
+
# interpreter session.
|
|
18
|
+
_sys.modules.setdefault("mssparkutils", _sys.modules[__name__])
|
|
19
|
+
_sys.modules.setdefault("mssparkutils.fs", fs)
|
|
@@ -0,0 +1,359 @@
|
|
|
1
|
+
"""notebookutils.fs — Fabric/Synapse-compatible file system utilities.
|
|
2
|
+
|
|
3
|
+
Memory-space operations are backed by ``fsspec`` (s3fs, adlfs, gcsfs, local)
|
|
4
|
+
and POSIX mounting is backed by FUSE binaries (mount-s3, blobfuse2, gcsfuse).
|
|
5
|
+
|
|
6
|
+
Run ``notebookutils.fs.help()`` for an overview of the available methods.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import logging
|
|
12
|
+
import posixpath
|
|
13
|
+
import shutil
|
|
14
|
+
import subprocess
|
|
15
|
+
from typing import Any, Dict, List, Optional
|
|
16
|
+
|
|
17
|
+
from notebookutils.fs._client import (
|
|
18
|
+
get_canonical_protocol,
|
|
19
|
+
get_fs_and_path,
|
|
20
|
+
get_fs_client,
|
|
21
|
+
strip_protocol_for_local,
|
|
22
|
+
)
|
|
23
|
+
from notebookutils.fs._fileinfo import FileInfo, MountPointInfo
|
|
24
|
+
from notebookutils.fs._mount import (
|
|
25
|
+
getMountPath,
|
|
26
|
+
mount,
|
|
27
|
+
mounts,
|
|
28
|
+
unmount,
|
|
29
|
+
)
|
|
30
|
+
from notebookutils.fs._registry import configure
|
|
31
|
+
|
|
32
|
+
# Suppress underlying vendor telemetry noise
|
|
33
|
+
logging.getLogger("azure.identity").setLevel(logging.ERROR)
|
|
34
|
+
logging.getLogger("google.auth").setLevel(logging.ERROR)
|
|
35
|
+
|
|
36
|
+
__all__ = [
|
|
37
|
+
"ls",
|
|
38
|
+
"mkdirs",
|
|
39
|
+
"cp",
|
|
40
|
+
"fastcp",
|
|
41
|
+
"mv",
|
|
42
|
+
"put",
|
|
43
|
+
"head",
|
|
44
|
+
"append",
|
|
45
|
+
"rm",
|
|
46
|
+
"exists",
|
|
47
|
+
"getProperties",
|
|
48
|
+
"help",
|
|
49
|
+
"mount",
|
|
50
|
+
"unmount",
|
|
51
|
+
"mounts",
|
|
52
|
+
"getMountPath",
|
|
53
|
+
"configure",
|
|
54
|
+
"FileInfo",
|
|
55
|
+
"MountPointInfo",
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
_COPY_CHUNK_SIZE = 8 * 1024 * 1024 # 8 MiB streaming buffer
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------------------------------------------------------------------------
|
|
62
|
+
# Helpers
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
def _to_fileinfo(entry: Dict[str, Any], protocol: str) -> FileInfo:
|
|
66
|
+
raw_path = entry.get("name", "")
|
|
67
|
+
is_dir = entry.get("type") == "directory"
|
|
68
|
+
size = int(entry.get("size") or 0)
|
|
69
|
+
|
|
70
|
+
name = posixpath.basename(raw_path.rstrip("/"))
|
|
71
|
+
if protocol != "file" and "://" not in raw_path:
|
|
72
|
+
display_path = f"{protocol}://{raw_path}"
|
|
73
|
+
else:
|
|
74
|
+
display_path = raw_path
|
|
75
|
+
|
|
76
|
+
mtime = entry.get("mtime") or entry.get("last_modified") or entry.get("LastModified")
|
|
77
|
+
modify_time: Optional[int] = None
|
|
78
|
+
if mtime is not None:
|
|
79
|
+
try:
|
|
80
|
+
modify_time = int(mtime.timestamp() * 1000) # datetime-like
|
|
81
|
+
except AttributeError:
|
|
82
|
+
try:
|
|
83
|
+
modify_time = int(float(mtime) * 1000)
|
|
84
|
+
except (TypeError, ValueError):
|
|
85
|
+
modify_time = None
|
|
86
|
+
|
|
87
|
+
return FileInfo(
|
|
88
|
+
path=display_path,
|
|
89
|
+
name=name,
|
|
90
|
+
size=size,
|
|
91
|
+
isDir=is_dir,
|
|
92
|
+
modifyTime=modify_time,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_dir(fs: Any, path: str) -> bool:
|
|
97
|
+
try:
|
|
98
|
+
return fs.isdir(path)
|
|
99
|
+
except Exception:
|
|
100
|
+
return False
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
# ---------------------------------------------------------------------------
|
|
104
|
+
# Memory-space public API (Fabric-compatible signatures)
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
def ls(path: str) -> List[FileInfo]:
|
|
108
|
+
"""List the contents of a directory as :class:`FileInfo` objects."""
|
|
109
|
+
fs, native = get_fs_and_path(path)
|
|
110
|
+
protocol = get_canonical_protocol(path)
|
|
111
|
+
entries = fs.ls(native, detail=True)
|
|
112
|
+
return [_to_fileinfo(e, protocol) for e in entries]
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def mkdirs(path: str) -> bool:
|
|
116
|
+
"""Create the given directory and any necessary parent directories."""
|
|
117
|
+
fs, native = get_fs_and_path(path)
|
|
118
|
+
fs.makedirs(native, exist_ok=True)
|
|
119
|
+
return True
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def exists(path: str) -> bool:
|
|
123
|
+
"""Check whether a file or directory exists at the given path."""
|
|
124
|
+
fs, native = get_fs_and_path(path)
|
|
125
|
+
return bool(fs.exists(native))
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def rm(path: str, recurse: bool = False) -> bool:
|
|
129
|
+
"""Remove a file or directory. Set ``recurse=True`` for directories."""
|
|
130
|
+
fs, native = get_fs_and_path(path)
|
|
131
|
+
fs.rm(native, recursive=recurse)
|
|
132
|
+
return True
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def put(file: str, content: str, overwrite: bool = False) -> bool:
|
|
136
|
+
"""Write the given string out to a file, encoded in UTF-8."""
|
|
137
|
+
fs, native = get_fs_and_path(file)
|
|
138
|
+
if not overwrite and fs.exists(native):
|
|
139
|
+
raise FileExistsError(
|
|
140
|
+
f"File already exists: {file}. Pass overwrite=True to replace it."
|
|
141
|
+
)
|
|
142
|
+
parent = posixpath.dirname(native.rstrip("/"))
|
|
143
|
+
if parent:
|
|
144
|
+
try:
|
|
145
|
+
fs.makedirs(parent, exist_ok=True)
|
|
146
|
+
except Exception:
|
|
147
|
+
pass
|
|
148
|
+
with fs.open(native, "wb") as f:
|
|
149
|
+
f.write(content.encode("utf-8"))
|
|
150
|
+
return True
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def append(file: str, content: str, createFileIfNotExists: bool = False) -> bool:
|
|
154
|
+
"""Append the content to a file, encoded in UTF-8.
|
|
155
|
+
|
|
156
|
+
.. note:: Not safe for concurrent writes to the same file. Object-store
|
|
157
|
+
backends lack a native append primitive, so this performs
|
|
158
|
+
read-modify-write there; local files use a true O_APPEND.
|
|
159
|
+
"""
|
|
160
|
+
fs, native = get_fs_and_path(file)
|
|
161
|
+
file_exists = fs.exists(native)
|
|
162
|
+
if not file_exists and not createFileIfNotExists:
|
|
163
|
+
raise FileNotFoundError(
|
|
164
|
+
f"File not found: {file}. Pass createFileIfNotExists=True to create it."
|
|
165
|
+
)
|
|
166
|
+
protocol = get_canonical_protocol(file)
|
|
167
|
+
data = content.encode("utf-8")
|
|
168
|
+
if protocol == "file":
|
|
169
|
+
with fs.open(native, "ab") as f:
|
|
170
|
+
f.write(data)
|
|
171
|
+
else:
|
|
172
|
+
existing = fs.cat_file(native) if file_exists else b""
|
|
173
|
+
with fs.open(native, "wb") as f:
|
|
174
|
+
f.write(existing + data)
|
|
175
|
+
return True
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def head(file: str, max_bytes: int = 1024 * 100) -> str:
|
|
179
|
+
"""Return up to the first ``max_bytes`` bytes of the file as a UTF-8 string."""
|
|
180
|
+
fs, native = get_fs_and_path(file)
|
|
181
|
+
with fs.open(native, "rb") as f:
|
|
182
|
+
data = f.read(max_bytes)
|
|
183
|
+
return data.decode("utf-8", errors="replace")
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def getProperties(path: str) -> Dict[str, Any]:
|
|
187
|
+
"""Get the metadata properties of the given path as a dictionary."""
|
|
188
|
+
fs, native = get_fs_and_path(path)
|
|
189
|
+
info = dict(fs.info(native))
|
|
190
|
+
# Normalize non-JSON-friendly values to strings for portability
|
|
191
|
+
return {k: (v if isinstance(v, (str, int, float, bool, list, dict, type(None))) else str(v)) for k, v in info.items()}
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def cp(src: str, dest: str, recurse: bool = False) -> bool:
|
|
195
|
+
"""Copy a file or directory, possibly across file systems / clouds.
|
|
196
|
+
|
|
197
|
+
Same-protocol copies use the backend's native copy. Cross-protocol copies
|
|
198
|
+
stream through memory in chunks — no intermediate files are written to
|
|
199
|
+
local disk.
|
|
200
|
+
"""
|
|
201
|
+
src_proto = get_canonical_protocol(src)
|
|
202
|
+
dest_proto = get_canonical_protocol(dest)
|
|
203
|
+
|
|
204
|
+
if src_proto == dest_proto:
|
|
205
|
+
fs, native_src = get_fs_and_path(src)
|
|
206
|
+
_, native_dest = get_fs_and_path(dest)
|
|
207
|
+
if _is_dir(fs, native_src) and not recurse:
|
|
208
|
+
raise IsADirectoryError(
|
|
209
|
+
f"{src} is a directory. Pass recurse=True to copy directories."
|
|
210
|
+
)
|
|
211
|
+
fs.cp(native_src, native_dest, recursive=recurse)
|
|
212
|
+
return True
|
|
213
|
+
|
|
214
|
+
# Cross-provider streaming bridge
|
|
215
|
+
src_fs, native_src = get_fs_and_path(src)
|
|
216
|
+
dest_fs, native_dest = get_fs_and_path(dest)
|
|
217
|
+
|
|
218
|
+
if _is_dir(src_fs, native_src):
|
|
219
|
+
if not recurse:
|
|
220
|
+
raise IsADirectoryError(
|
|
221
|
+
f"{src} is a directory. Pass recurse=True to copy directories."
|
|
222
|
+
)
|
|
223
|
+
base = native_src.rstrip("/")
|
|
224
|
+
for src_file in src_fs.find(base):
|
|
225
|
+
rel = src_file[len(base) :].lstrip("/")
|
|
226
|
+
target = posixpath.join(native_dest.rstrip("/"), rel)
|
|
227
|
+
_stream_copy_file(src_fs, src_file, dest_fs, target)
|
|
228
|
+
else:
|
|
229
|
+
_stream_copy_file(src_fs, native_src, dest_fs, native_dest)
|
|
230
|
+
return True
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _stream_copy_file(src_fs: Any, src_path: str, dest_fs: Any, dest_path: str) -> None:
|
|
234
|
+
parent = posixpath.dirname(dest_path.rstrip("/"))
|
|
235
|
+
if parent:
|
|
236
|
+
try:
|
|
237
|
+
dest_fs.makedirs(parent, exist_ok=True)
|
|
238
|
+
except Exception:
|
|
239
|
+
pass
|
|
240
|
+
with src_fs.open(src_path, "rb") as rf, dest_fs.open(dest_path, "wb") as wf:
|
|
241
|
+
shutil.copyfileobj(rf, wf, _COPY_CHUNK_SIZE)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def fastcp(
|
|
245
|
+
src: str,
|
|
246
|
+
dest: str,
|
|
247
|
+
recurse: bool = True,
|
|
248
|
+
extraConfigs: Optional[Dict[str, Any]] = None,
|
|
249
|
+
) -> bool:
|
|
250
|
+
"""Copy via ``azcopy`` for better performance with large data volumes.
|
|
251
|
+
|
|
252
|
+
Falls back to :func:`cp` transparently when the ``azcopy`` binary is not
|
|
253
|
+
available or when neither endpoint is Azure/local.
|
|
254
|
+
"""
|
|
255
|
+
azcopy = shutil.which("azcopy")
|
|
256
|
+
src_proto = get_canonical_protocol(src)
|
|
257
|
+
dest_proto = get_canonical_protocol(dest)
|
|
258
|
+
azcopy_protos = {"file", "abfs", "az", "https"}
|
|
259
|
+
|
|
260
|
+
if (
|
|
261
|
+
azcopy
|
|
262
|
+
and src_proto in azcopy_protos
|
|
263
|
+
and dest_proto in azcopy_protos
|
|
264
|
+
and "abfs" in (src_proto, dest_proto)
|
|
265
|
+
):
|
|
266
|
+
cmd = [azcopy, "copy", _azcopy_url(src), _azcopy_url(dest)]
|
|
267
|
+
if recurse:
|
|
268
|
+
cmd.append("--recursive")
|
|
269
|
+
result = subprocess.run(cmd, capture_output=True, text=True)
|
|
270
|
+
if result.returncode == 0:
|
|
271
|
+
return True
|
|
272
|
+
logging.getLogger(__name__).warning(
|
|
273
|
+
"azcopy failed (%s); falling back to cp()", result.stderr.strip()[:500]
|
|
274
|
+
)
|
|
275
|
+
return cp(src, dest, recurse=recurse)
|
|
276
|
+
|
|
277
|
+
|
|
278
|
+
def _azcopy_url(path: str) -> str:
|
|
279
|
+
proto = get_canonical_protocol(path)
|
|
280
|
+
if proto == "file":
|
|
281
|
+
return strip_protocol_for_local(path)
|
|
282
|
+
if proto == "abfs":
|
|
283
|
+
clean = path.split("://", 1)[1]
|
|
284
|
+
if "@" in clean:
|
|
285
|
+
container, _, host_and_path = clean.partition("@")
|
|
286
|
+
host, _, sub = host_and_path.partition("/")
|
|
287
|
+
account = host.split(".", 1)[0]
|
|
288
|
+
url = f"https://{account}.blob.core.windows.net/{container}"
|
|
289
|
+
return f"{url}/{sub}" if sub else url
|
|
290
|
+
return path
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def mv(
|
|
294
|
+
src: str,
|
|
295
|
+
dest: str,
|
|
296
|
+
create_path: bool = False,
|
|
297
|
+
overwrite: bool = False,
|
|
298
|
+
) -> bool:
|
|
299
|
+
"""Move a file or directory, possibly across file systems."""
|
|
300
|
+
src_proto = get_canonical_protocol(src)
|
|
301
|
+
dest_proto = get_canonical_protocol(dest)
|
|
302
|
+
|
|
303
|
+
dest_fs, native_dest = get_fs_and_path(dest)
|
|
304
|
+
if dest_fs.exists(native_dest) and not overwrite:
|
|
305
|
+
raise FileExistsError(
|
|
306
|
+
f"Destination already exists: {dest}. Pass overwrite=True to replace it."
|
|
307
|
+
)
|
|
308
|
+
parent = posixpath.dirname(native_dest.rstrip("/"))
|
|
309
|
+
if parent and not dest_fs.exists(parent):
|
|
310
|
+
if create_path:
|
|
311
|
+
dest_fs.makedirs(parent, exist_ok=True)
|
|
312
|
+
elif dest_proto == "file":
|
|
313
|
+
# Object stores have virtual directories; only real local/POSIX
|
|
314
|
+
# filesystems require the parent to pre-exist.
|
|
315
|
+
raise FileNotFoundError(
|
|
316
|
+
f"Parent directory does not exist: {parent}. "
|
|
317
|
+
"Pass create_path=True to create it."
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
src_fs, native_src = get_fs_and_path(src)
|
|
321
|
+
recurse = _is_dir(src_fs, native_src)
|
|
322
|
+
|
|
323
|
+
if dest_fs.exists(native_dest) and overwrite:
|
|
324
|
+
dest_fs.rm(native_dest, recursive=True)
|
|
325
|
+
|
|
326
|
+
if src_proto == dest_proto:
|
|
327
|
+
src_fs.mv(native_src, native_dest, recursive=recurse)
|
|
328
|
+
else:
|
|
329
|
+
cp(src, dest, recurse=recurse)
|
|
330
|
+
src_fs.rm(native_src, recursive=recurse)
|
|
331
|
+
return True
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def help(method: Optional[str] = None) -> None:
|
|
335
|
+
"""Print an overview of the available notebookutils.fs methods."""
|
|
336
|
+
table = {
|
|
337
|
+
"ls": "ls(path: str) -> List[FileInfo]: Lists the contents of a directory.",
|
|
338
|
+
"mkdirs": "mkdirs(path: str) -> bool: Creates the directory and necessary parents.",
|
|
339
|
+
"cp": "cp(src: str, dest: str, recurse: bool = False) -> bool: Copies a file or directory, possibly across file systems.",
|
|
340
|
+
"fastcp": "fastcp(src: str, dest: str, recurse: bool = True, extraConfigs: dict = None) -> bool: Copies via azcopy for better performance; falls back to cp().",
|
|
341
|
+
"mv": "mv(src: str, dest: str, create_path: bool = False, overwrite: bool = False) -> bool: Moves a file or directory, possibly across file systems.",
|
|
342
|
+
"put": "put(file: str, content: str, overwrite: bool = False) -> bool: Writes the given string out to a file, encoded in UTF-8.",
|
|
343
|
+
"head": "head(file: str, max_bytes: int = 102400) -> str: Returns up to the first max_bytes of the file as a UTF-8 string.",
|
|
344
|
+
"append": "append(file: str, content: str, createFileIfNotExists: bool = False) -> bool: Appends the content to a file.",
|
|
345
|
+
"rm": "rm(path: str, recurse: bool = False) -> bool: Removes a file or directory.",
|
|
346
|
+
"exists": "exists(path: str) -> bool: Checks if a file or directory exists.",
|
|
347
|
+
"getProperties": "getProperties(path: str) -> dict: Gets the metadata properties of the given path.",
|
|
348
|
+
"mount": "mount(source: str, mount_point: str, extra_configs: dict = None) -> bool: Mounts remote storage at a local POSIX path.",
|
|
349
|
+
"unmount": "unmount(mount_point: str, extra_configs: dict = None) -> bool: Unmounts and removes a mount point.",
|
|
350
|
+
"mounts": "mounts() -> List[MountPointInfo]: Lists all existing mount points.",
|
|
351
|
+
"getMountPath": "getMountPath(mount_point: str, scope: str = '') -> str: Gets the local path for a mount point.",
|
|
352
|
+
"configure": "configure(protocol: str, **kwargs) -> None: Overrides credential/config discovery for a protocol.",
|
|
353
|
+
}
|
|
354
|
+
if method:
|
|
355
|
+
print(table.get(method, f"Unknown method: {method}"))
|
|
356
|
+
return
|
|
357
|
+
print("notebookutils.fs provides file system utilities backed by fsspec and FUSE.\n")
|
|
358
|
+
for line in table.values():
|
|
359
|
+
print(f" {line}")
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Protocol parsing and fsspec client resolution."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any, Tuple
|
|
6
|
+
|
|
7
|
+
from notebookutils.fs import _registry
|
|
8
|
+
|
|
9
|
+
# Maps canonical protocol -> (pip extra, backend package) for error messages.
|
|
10
|
+
_BACKEND_HINTS = {
|
|
11
|
+
"s3": ("s3", "s3fs"),
|
|
12
|
+
"abfs": ("azure", "adlfs"),
|
|
13
|
+
"az": ("azure", "adlfs"),
|
|
14
|
+
"gcs": ("gcs", "gcsfs"),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_protocol(path: str) -> str:
|
|
19
|
+
"""Extract the raw protocol scheme from a path (without normalizing)."""
|
|
20
|
+
if "://" in path:
|
|
21
|
+
return path.split("://", 1)[0]
|
|
22
|
+
if path.startswith("file:"):
|
|
23
|
+
return "file"
|
|
24
|
+
return "file"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def get_canonical_protocol(path: str) -> str:
|
|
28
|
+
return _registry.normalize_protocol(get_protocol(path))
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def strip_protocol_for_local(path: str) -> str:
|
|
32
|
+
"""For ``file:`` URLs return the plain OS path; otherwise return as-is."""
|
|
33
|
+
if path.startswith("file://"):
|
|
34
|
+
return path[len("file://") :] or "/"
|
|
35
|
+
if path.startswith("file:"):
|
|
36
|
+
return path[len("file:") :]
|
|
37
|
+
return path
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_fs_client(path: str) -> Any:
|
|
41
|
+
"""Resolve and instantiate the proper fsspec storage client wrapper."""
|
|
42
|
+
import fsspec
|
|
43
|
+
|
|
44
|
+
protocol = get_canonical_protocol(path)
|
|
45
|
+
cached = _registry.get_cached_instance(protocol)
|
|
46
|
+
if cached is not None:
|
|
47
|
+
return cached
|
|
48
|
+
|
|
49
|
+
config = _registry.get_fsspec_options(protocol)
|
|
50
|
+
try:
|
|
51
|
+
instance = fsspec.filesystem(protocol, **config)
|
|
52
|
+
except ImportError as exc:
|
|
53
|
+
extra, pkg = _BACKEND_HINTS.get(protocol, (None, None))
|
|
54
|
+
if extra:
|
|
55
|
+
raise ImportError(
|
|
56
|
+
f"The '{protocol}' protocol requires the '{pkg}' package. "
|
|
57
|
+
f"Install it with: pip install notebookutils[{extra}]"
|
|
58
|
+
) from exc
|
|
59
|
+
raise
|
|
60
|
+
_registry.set_cached_instance(protocol, instance)
|
|
61
|
+
return instance
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def get_fs_and_path(path: str) -> Tuple[Any, str]:
|
|
65
|
+
"""Return ``(fsspec_filesystem, backend_native_path)`` for a user path."""
|
|
66
|
+
fs = get_fs_client(path)
|
|
67
|
+
if get_canonical_protocol(path) == "file":
|
|
68
|
+
return fs, strip_protocol_for_local(path)
|
|
69
|
+
return fs, path
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"""FileInfo and MountPointInfo data structures (Fabric-compatible)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from typing import Any, Dict, Optional
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class FileInfo:
|
|
11
|
+
"""Describes a file system entry, mirroring Fabric's notebookutils.
|
|
12
|
+
|
|
13
|
+
Attributes match the Fabric usage pattern::
|
|
14
|
+
|
|
15
|
+
for f in notebookutils.fs.ls(path):
|
|
16
|
+
print(f.name, f.isDir, f.isFile, f.path, f.size)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
path: str
|
|
20
|
+
name: str
|
|
21
|
+
size: int
|
|
22
|
+
isDir: bool
|
|
23
|
+
modifyTime: Optional[int] = None
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def isFile(self) -> bool:
|
|
27
|
+
return not self.isDir
|
|
28
|
+
|
|
29
|
+
def __repr__(self) -> str: # compact, notebook-friendly
|
|
30
|
+
kind = "dir" if self.isDir else "file"
|
|
31
|
+
return f"FileInfo(path={self.path!r}, name={self.name!r}, size={self.size}, {kind})"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass(frozen=True)
|
|
35
|
+
class MountPointInfo:
|
|
36
|
+
"""Describes an active mount point, mirroring Fabric's ``mounts()`` output."""
|
|
37
|
+
|
|
38
|
+
mountPoint: str
|
|
39
|
+
source: str
|
|
40
|
+
localPath: str
|
|
41
|
+
extraConfigs: Dict[str, Any] = field(default_factory=dict)
|
|
42
|
+
|
|
43
|
+
def __repr__(self) -> str:
|
|
44
|
+
return (
|
|
45
|
+
f"MountPointInfo(mountPoint={self.mountPoint!r}, "
|
|
46
|
+
f"source={self.source!r}, localPath={self.localPath!r})"
|
|
47
|
+
)
|