fss-cli 0.11.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.
fss_cli/_ls.py ADDED
@@ -0,0 +1,435 @@
1
+ """Listing execution for the central ``ls`` callback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from collections.abc import Mapping
8
+ from dataclasses import dataclass
9
+ from functools import partial
10
+ from typing import TYPE_CHECKING, Generic, TypeAlias, TypeVar, cast
11
+
12
+ import typer
13
+
14
+ from ._command import (
15
+ _collate,
16
+ _CommandFailureError,
17
+ _drain_current_operation,
18
+ _Failure,
19
+ _first_backend_error,
20
+ _MappedOperand,
21
+ _render_failure,
22
+ _render_output_failure,
23
+ _run_mapped_command,
24
+ )
25
+ from ._concurrent import _gather_bounded
26
+ from ._listing import ListingRow, emphasize, render_listing, to_listing
27
+ from ._metadata import valid_display_text
28
+
29
+ if TYPE_CHECKING:
30
+ from collections.abc import Awaitable, Callable
31
+
32
+ from fsspec.asyn import AsyncFileSystem
33
+
34
+ from ._app import AsyncFilesystemSource
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class _LsRequest:
39
+ include_almost_all: bool
40
+ long_listing: bool
41
+ human_readable: bool
42
+ operands: tuple[_MappedOperand, ...]
43
+
44
+
45
+ _PayloadT = TypeVar("_PayloadT")
46
+
47
+
48
+ @dataclass(frozen=True)
49
+ class _FileResult(Generic[_PayloadT]):
50
+ operand: _MappedOperand
51
+ value: _PayloadT
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class _DirectoryResult(Generic[_PayloadT]):
56
+ operand: _MappedOperand
57
+ values: tuple[_PayloadT, ...]
58
+
59
+
60
+ @dataclass(frozen=True)
61
+ class _PlainName:
62
+ text: str
63
+ directory: bool = False
64
+
65
+
66
+ _PlainResult: TypeAlias = _FileResult[_PlainName] | _DirectoryResult[_PlainName]
67
+ _LongResult: TypeAlias = _FileResult[ListingRow] | _DirectoryResult[ListingRow]
68
+
69
+
70
+ def _emphasis_enabled() -> bool:
71
+ """Decide once per invocation whether directory names may be bold."""
72
+ stdout = sys.stdout
73
+ try:
74
+ interactive = stdout is not None and stdout.isatty()
75
+ except (AttributeError, ValueError):
76
+ return False
77
+ return interactive and not os.environ.get("NO_COLOR") and os.environ.get("TERM") not in {"dumb", "unknown"}
78
+
79
+
80
+ async def _run_ls(
81
+ command: str,
82
+ request: _LsRequest,
83
+ sources: Mapping[str, AsyncFilesystemSource],
84
+ ) -> None:
85
+ emphasis = _emphasis_enabled()
86
+
87
+ async def execute(filesystems: Mapping[str, AsyncFileSystem]) -> None:
88
+ failures, format_output = await _trace_and_prepare(request, filesystems, emphasis=emphasis)
89
+ backend_error = _first_backend_error(failures)
90
+ output_error = None
91
+ try:
92
+ output = format_output()
93
+ for failure in failures:
94
+ _render_failure(command, failure)
95
+ if output:
96
+ try:
97
+ typer.echo(output, nl=False, color=True)
98
+ except BrokenPipeError as error:
99
+ output_error = error
100
+ except Exception as error: # noqa: BLE001 - output boundary.
101
+ output_error = error
102
+ _render_output_failure(command, error)
103
+ except Exception as error: # Preserve backend/output cause through cleanup.
104
+ command_error = backend_error if backend_error is not None else output_error
105
+ if command_error is None:
106
+ command_error = error
107
+ raise _CommandFailureError(
108
+ error=command_error,
109
+ render=False,
110
+ propagate=error,
111
+ ) from error
112
+ if failures or output_error is not None:
113
+ raise _CommandFailureError(
114
+ error=backend_error if backend_error is not None else output_error,
115
+ render=False,
116
+ )
117
+
118
+ await _run_mapped_command(
119
+ command,
120
+ request.operands,
121
+ sources,
122
+ execute,
123
+ broken_pipe_exit_code=1,
124
+ )
125
+
126
+
127
+ async def _trace_and_prepare(
128
+ request: _LsRequest,
129
+ filesystems: Mapping[str, AsyncFileSystem],
130
+ *,
131
+ emphasis: bool,
132
+ ) -> tuple[tuple[_Failure, ...], Callable[[], str]]:
133
+ # The reader/renderer pairing is fixed per mode; branching keeps each pair
134
+ # monomorphic while the deferred formatter preserves the caller's error
135
+ # flow (formatting still happens inside the caller's try block).
136
+ multiple_operands = len(request.operands) > 1
137
+ if request.long_listing:
138
+
139
+ def render_long(rows: tuple[ListingRow, ...]) -> str:
140
+ return render_listing(
141
+ rows,
142
+ human_readable=request.human_readable,
143
+ emphasize_directories=emphasis,
144
+ ).removesuffix("\n")
145
+
146
+ long_successes, failures = await _trace_operands(request, filesystems, _read_long_operand)
147
+ return failures, partial(
148
+ _format_successes,
149
+ long_successes,
150
+ render_long,
151
+ multiple_operands=multiple_operands,
152
+ emphasis=emphasis,
153
+ )
154
+
155
+ def render_plain(names: tuple[_PlainName, ...]) -> str:
156
+ return "\n".join(emphasize(name.text) if emphasis and name.directory else name.text for name in names)
157
+
158
+ plain_successes, failures = await _trace_operands(request, filesystems, _read_plain_operand)
159
+ return failures, partial(
160
+ _format_successes,
161
+ plain_successes,
162
+ render_plain,
163
+ multiple_operands=multiple_operands,
164
+ emphasis=emphasis,
165
+ )
166
+
167
+
168
+ async def _trace_operands(
169
+ request: _LsRequest,
170
+ filesystems: Mapping[str, AsyncFileSystem],
171
+ read: Callable[
172
+ ...,
173
+ Awaitable[_FileResult[_PayloadT] | _DirectoryResult[_PayloadT] | _Failure],
174
+ ],
175
+ ) -> tuple[
176
+ tuple[_FileResult[_PayloadT] | _DirectoryResult[_PayloadT], ...],
177
+ tuple[_Failure, ...],
178
+ ]:
179
+ # Operands are independent reads, so they overlap under the shared bound;
180
+ # results come back in operand order and nothing is written until all are
181
+ # in, so output and diagnostics are unchanged by completion order.
182
+ results = await _gather_bounded(
183
+ [
184
+ partial(
185
+ read,
186
+ operand,
187
+ filesystems[operand.name],
188
+ include_almost_all=request.include_almost_all,
189
+ )
190
+ for operand in request.operands
191
+ ]
192
+ )
193
+ successes: list[_FileResult[_PayloadT] | _DirectoryResult[_PayloadT]] = []
194
+ failures = []
195
+ for result in results:
196
+ if isinstance(result, _Failure):
197
+ failures.append(result)
198
+ else:
199
+ successes.append(result)
200
+ return tuple(successes), tuple(failures)
201
+
202
+
203
+ async def _classify_operand(
204
+ operand: _MappedOperand,
205
+ filesystem: AsyncFileSystem,
206
+ ) -> Mapping[str, object] | _Failure:
207
+ # fsspec's native async API intentionally exposes underscore coroutines.
208
+ try:
209
+ info = await _drain_current_operation(filesystem._info(operand.path))
210
+ except Exception as error: # noqa: BLE001 - classify awaited backend failure.
211
+ return _Failure(operand, backend_error=error)
212
+
213
+ if not isinstance(info, Mapping):
214
+ return _Failure(operand)
215
+ kind = info.get("type")
216
+ if type(kind) is not str or (kind not in {"file", "directory"} and info.get("islink") is not True):
217
+ return _Failure(operand)
218
+ return cast("Mapping[str, object]", info)
219
+
220
+
221
+ def _file_result_from_info(
222
+ operand: _MappedOperand,
223
+ info: Mapping[str, object],
224
+ *,
225
+ long_listing: bool,
226
+ ) -> _PlainResult | _LongResult | _Failure:
227
+ if not long_listing:
228
+ return _FileResult(operand=operand, value=_PlainName(operand.spelling))
229
+ row = _listing_row(info)
230
+ if row is None or not row.name or "\0" in row.name or "\n" in row.name:
231
+ return _Failure(operand)
232
+ return _FileResult(operand=operand, value=row)
233
+
234
+
235
+ async def _read_operand(
236
+ operand: _MappedOperand,
237
+ filesystem: AsyncFileSystem,
238
+ *,
239
+ include_almost_all: bool,
240
+ long_listing: bool,
241
+ ) -> _PlainResult | _LongResult | _Failure:
242
+ """Preserve operand identity before listing a directory's children."""
243
+ info = await _classify_operand(operand, filesystem)
244
+ if isinstance(info, _Failure):
245
+ return info
246
+ if info["type"] != "directory" or info.get("islink") is True:
247
+ return _file_result_from_info(operand, cast("Mapping[str, object]", info), long_listing=long_listing)
248
+ listing = await _list_directory(operand, filesystem, detail=True)
249
+ if isinstance(listing, list):
250
+ if long_listing:
251
+ rows = _directory_rows(operand.path, listing, include_almost_all=include_almost_all)
252
+ if rows is not None:
253
+ return _DirectoryResult(operand=operand, values=rows)
254
+ elif (children := _directory_lines(operand.path, listing, include_almost_all=include_almost_all)) is not None:
255
+ return _DirectoryResult(operand=operand, values=children)
256
+
257
+ if isinstance(listing, _Failure):
258
+ return listing
259
+ return _Failure(operand)
260
+
261
+
262
+ async def _read_plain_operand(
263
+ operand: _MappedOperand,
264
+ filesystem: AsyncFileSystem,
265
+ *,
266
+ include_almost_all: bool,
267
+ ) -> _PlainResult | _Failure:
268
+ result = await _read_operand(
269
+ operand,
270
+ filesystem,
271
+ include_almost_all=include_almost_all,
272
+ long_listing=False,
273
+ )
274
+ return cast("_PlainResult | _Failure", result)
275
+
276
+
277
+ async def _read_long_operand(
278
+ operand: _MappedOperand,
279
+ filesystem: AsyncFileSystem,
280
+ *,
281
+ include_almost_all: bool,
282
+ ) -> _LongResult | _Failure:
283
+ result = await _read_operand(
284
+ operand,
285
+ filesystem,
286
+ include_almost_all=include_almost_all,
287
+ long_listing=True,
288
+ )
289
+ return cast("_LongResult | _Failure", result)
290
+
291
+
292
+ def _listing_row(info: Mapping[str, object]) -> ListingRow | None:
293
+ try:
294
+ return to_listing(info)
295
+ except (TypeError, ValueError):
296
+ return None
297
+
298
+
299
+ async def _list_directory(
300
+ operand: _MappedOperand,
301
+ filesystem: AsyncFileSystem,
302
+ *,
303
+ detail: bool,
304
+ ) -> object | _Failure:
305
+ try:
306
+ return await _drain_current_operation(
307
+ filesystem._ls(
308
+ operand.path,
309
+ detail=detail,
310
+ )
311
+ )
312
+ except Exception as error: # noqa: BLE001 - classify awaited backend failure.
313
+ return _Failure(operand, backend_error=error)
314
+
315
+
316
+ def _sort_key(
317
+ result: _FileResult[_PayloadT] | _DirectoryResult[_PayloadT],
318
+ ) -> tuple[str, str]:
319
+ return _collate(result.operand.spelling)
320
+
321
+
322
+ def _format_successes(
323
+ successes: tuple[_FileResult[_PayloadT] | _DirectoryResult[_PayloadT], ...],
324
+ render: Callable[[tuple[_PayloadT, ...]], str],
325
+ *,
326
+ multiple_operands: bool,
327
+ emphasis: bool,
328
+ ) -> str:
329
+ if not successes:
330
+ return ""
331
+ if not multiple_operands:
332
+ result = successes[0]
333
+ values = (result.value,) if isinstance(result, _FileResult) else result.values
334
+ rendered = render(values)
335
+ return f"{rendered}\n" if rendered else ""
336
+
337
+ files = sorted(
338
+ (result for result in successes if isinstance(result, _FileResult)),
339
+ key=_sort_key,
340
+ )
341
+ directories = sorted(
342
+ (result for result in successes if isinstance(result, _DirectoryResult)),
343
+ key=_sort_key,
344
+ )
345
+ blocks: list[str] = []
346
+ file_values = tuple(result.value for result in files)
347
+ if file_values:
348
+ blocks.append(render(file_values))
349
+ for result in directories:
350
+ rendered = render(result.values)
351
+ spelling = result.operand.spelling
352
+ header = f"{emphasize(spelling) if emphasis else spelling}:"
353
+ blocks.append(f"{header}\n{rendered}" if rendered else header)
354
+ return _join_blocks(blocks)
355
+
356
+
357
+ def _join_blocks(blocks: list[str]) -> str:
358
+ return "\n\n".join(blocks) + "\n" if blocks else ""
359
+
360
+
361
+ def _directory_basename(path: str, name: object) -> str | None:
362
+ if not isinstance(name, str):
363
+ return None
364
+
365
+ comparison_path = path.rstrip("/")
366
+ prefix = "/" if not comparison_path else f"{comparison_path}/"
367
+ if not name.startswith(prefix):
368
+ return None
369
+ basename = name[len(prefix) :]
370
+ if not basename or "/" in basename or not valid_display_text(basename):
371
+ return None
372
+ return basename
373
+
374
+
375
+ def _directory_lines(
376
+ path: str,
377
+ listing: object,
378
+ *,
379
+ include_almost_all: bool,
380
+ ) -> tuple[_PlainName, ...] | None:
381
+ # Plain names print no metadata, so only name, type and islink are read;
382
+ # the rest of each record is deliberately left unvalidated.
383
+ if not isinstance(listing, list):
384
+ return None
385
+
386
+ names = []
387
+ for info in listing:
388
+ if not isinstance(info, Mapping):
389
+ return None
390
+ typed_info = cast("Mapping[str, object]", info)
391
+ basename = _directory_basename(path, typed_info.get("name"))
392
+ if basename is None:
393
+ return None
394
+ kind = typed_info.get("type")
395
+ directory = type(kind) is str and kind in {"directory", "dir"} and typed_info.get("islink") is not True
396
+ names.append(_PlainName(basename, directory=directory))
397
+
398
+ if include_almost_all:
399
+ selected = (name for name in names if name.text not in {".", ".."})
400
+ else:
401
+ selected = (name for name in names if not name.text.startswith("."))
402
+ return tuple(sorted(selected, key=lambda name: _collate(name.text)))
403
+
404
+
405
+ def _directory_rows(
406
+ path: str,
407
+ listing: object,
408
+ *,
409
+ include_almost_all: bool,
410
+ ) -> tuple[ListingRow, ...] | None:
411
+ if not isinstance(listing, list):
412
+ return None
413
+
414
+ entries: list[tuple[str, Mapping[str, object]]] = []
415
+ for info in listing:
416
+ if not isinstance(info, Mapping):
417
+ return None
418
+ typed_info = cast("Mapping[str, object]", info)
419
+ basename = _directory_basename(path, typed_info.get("name"))
420
+ if basename is None:
421
+ return None
422
+ entries.append((basename, typed_info))
423
+
424
+ if include_almost_all:
425
+ selected = (entry for entry in entries if entry[0] not in {".", ".."})
426
+ else:
427
+ selected = (entry for entry in entries if not entry[0].startswith("."))
428
+ sorted_entries = sorted(selected, key=lambda entry: _collate(entry[0]))
429
+ rows = []
430
+ for _basename, info in sorted_entries:
431
+ row = _listing_row(info)
432
+ if row is None:
433
+ return None
434
+ rows.append(row)
435
+ return tuple(rows)
fss_cli/_manifest.py ADDED
@@ -0,0 +1,264 @@
1
+ """Frozen, validated manifests built from a backend directory walk.
2
+
3
+ Recursive copy uses this module to validate and freeze a transfer plan before
4
+ writing any destination. Per
5
+ :doc:`ADR 0006 <../../../../docs/adr/0006-treat-backend-results-as-untrusted-input>`,
6
+ backend listings are untrusted. Invalid paths and duplicate children must fail
7
+ before copy can overwrite files at those paths.
8
+
9
+ The command reports incompatible results, unsupported entry types, and trees
10
+ above the entry limit separately.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from collections.abc import Mapping
16
+ from dataclasses import dataclass
17
+ from typing import TYPE_CHECKING, TypeAlias, cast
18
+
19
+ from ._metadata import snapshot_mapping
20
+ from ._path import (
21
+ _has_dot_segment,
22
+ _lexical_join,
23
+ _lexical_relative,
24
+ _same_lexical_path,
25
+ )
26
+ from ._walk import _IncompatibleListingError, _ListedRow, _ListingLimitError, _walk
27
+
28
+ if TYPE_CHECKING:
29
+ from fsspec.asyn import AsyncFileSystem
30
+
31
+
32
+ _MAX_ENTRIES = 10_000
33
+ _WALK_ROW_LENGTH = 3
34
+ _TOKEN_ALIASES = (
35
+ ("etag", ("ETag", "etag")),
36
+ ("md5", ("md5",)),
37
+ ("content-md5", ("content-md5", "content_md5")),
38
+ ("checksum", ("checksum",)),
39
+ )
40
+
41
+
42
+ class _IncompatibleResultError(Exception):
43
+ pass
44
+
45
+
46
+ class _UnsupportedEntryError(Exception):
47
+ pass
48
+
49
+
50
+ class _EntryLimitError(Exception):
51
+ pass
52
+
53
+
54
+ @dataclass(frozen=True)
55
+ class _ManifestEntry:
56
+ relative: str
57
+ path: str
58
+ kind: str
59
+ size: int | None
60
+ tokens: tuple[tuple[str, str | bytes], ...]
61
+
62
+
63
+ _Manifest: TypeAlias = tuple[_ManifestEntry, ...]
64
+
65
+
66
+ @dataclass(frozen=True)
67
+ class _WalkRow:
68
+ root: str
69
+ entries: tuple[_ManifestEntry, ...]
70
+ directory_paths: tuple[str, ...]
71
+
72
+
73
+ def _shared_tokens_match(
74
+ source_tokens: tuple[tuple[str, object], ...],
75
+ destination_tokens: Mapping[str, object],
76
+ ) -> bool:
77
+ """Check that every verification token present on both sides agrees."""
78
+ return all(destination_tokens[name] == value for name, value in source_tokens if name in destination_tokens)
79
+
80
+
81
+ def _tokens(info: Mapping[object, object]) -> tuple[tuple[str, str | bytes], ...]:
82
+ tokens: list[tuple[str, str | bytes]] = []
83
+ for normalized, aliases in _TOKEN_ALIASES:
84
+ present = [alias for alias in aliases if alias in info]
85
+ if len(present) > 1:
86
+ raise _IncompatibleResultError
87
+ if present:
88
+ value = info[present[0]]
89
+ if type(value) is not str and type(value) is not bytes:
90
+ raise _IncompatibleResultError
91
+ tokens.append((normalized, value))
92
+ return tuple(tokens)
93
+
94
+
95
+ def _entry(
96
+ relative: str,
97
+ path: str,
98
+ info: object,
99
+ *,
100
+ expected_kind: str | None = None,
101
+ ) -> _ManifestEntry:
102
+ if not isinstance(info, Mapping):
103
+ raise _IncompatibleResultError
104
+ name = info.get("name")
105
+ if not isinstance(name, str) or not _same_lexical_path(name, path):
106
+ raise _IncompatibleResultError
107
+ try:
108
+ typed_info = snapshot_mapping(cast("Mapping[object, object]", info))
109
+ except Exception as error:
110
+ raise _IncompatibleResultError from error
111
+ islink = typed_info.get("islink", False)
112
+ if type(islink) is not bool:
113
+ raise _IncompatibleResultError
114
+ kind = typed_info.get("type")
115
+ if not isinstance(kind, str):
116
+ raise _IncompatibleResultError
117
+ if islink or kind not in {"directory", "file"}:
118
+ raise _UnsupportedEntryError
119
+ if expected_kind is not None and kind != expected_kind:
120
+ raise _IncompatibleResultError
121
+ size = None
122
+ if kind == "file":
123
+ size = typed_info.get("size")
124
+ if type(size) is not int or size < 0:
125
+ raise _IncompatibleResultError
126
+ return _ManifestEntry(relative, path, kind, size, _tokens(typed_info))
127
+
128
+
129
+ def _walk_row(
130
+ source_path: str,
131
+ value: object,
132
+ *,
133
+ entry_capacity: int,
134
+ ) -> _WalkRow:
135
+ if type(value) is not tuple or len(value) != _WALK_ROW_LENGTH:
136
+ raise _IncompatibleResultError
137
+ root, directories, files = value
138
+ if (
139
+ type(root) is not str
140
+ or not root.startswith("/")
141
+ or "\0" in root
142
+ or "\n" in root
143
+ or "\r" in root
144
+ or _has_dot_segment(root)
145
+ or not isinstance(directories, Mapping)
146
+ or not isinstance(files, Mapping)
147
+ ):
148
+ raise _IncompatibleResultError
149
+
150
+ entries: list[_ManifestEntry] = []
151
+ directory_paths: list[str] = []
152
+ child_names: set[str] = set()
153
+ for collection, kind in ((directories, "directory"), (files, "file")):
154
+ if len(collection) > entry_capacity - len(entries):
155
+ raise _EntryLimitError
156
+ try:
157
+ children = snapshot_mapping(collection)
158
+ except Exception as error:
159
+ raise _IncompatibleResultError from error
160
+ for name, info in children.items():
161
+ if (
162
+ type(name) is not str
163
+ or not name
164
+ or name in {".", ".."}
165
+ or "/" in name
166
+ or "\0" in name
167
+ or "\n" in name
168
+ or "\r" in name
169
+ or name in child_names
170
+ ):
171
+ raise _IncompatibleResultError
172
+ child_names.add(name)
173
+ path = _lexical_join(root, name)
174
+ entry = _entry(
175
+ _relative_path(source_path, path),
176
+ path,
177
+ info,
178
+ expected_kind=kind,
179
+ )
180
+ entries.append(entry)
181
+ if len(entries) > entry_capacity:
182
+ raise _EntryLimitError
183
+ if kind == "directory":
184
+ directory_paths.append(path)
185
+ return _WalkRow(root, tuple(entries), tuple(directory_paths))
186
+
187
+
188
+ class _ManifestBuilder:
189
+ """Accept each frozen row once and retain only the final entry index."""
190
+
191
+ def __init__(self, root: _ManifestEntry) -> None:
192
+ self.entries = {"": root}
193
+ self.roots: set[str] = set()
194
+ self.expected_roots = {root.path}
195
+
196
+ @property
197
+ def capacity(self) -> int:
198
+ return _MAX_ENTRIES - len(self.entries)
199
+
200
+ def accept(self, row: _WalkRow) -> None:
201
+ if (
202
+ row.root in self.roots
203
+ or row.root not in self.expected_roots
204
+ or any(entry.relative in self.entries for entry in row.entries)
205
+ ):
206
+ raise _IncompatibleResultError
207
+ self.roots.add(row.root)
208
+ self.expected_roots.update(row.directory_paths)
209
+ self.entries.update((entry.relative, entry) for entry in row.entries)
210
+
211
+ def finish(self) -> _Manifest:
212
+ if self.roots != self.expected_roots:
213
+ raise _IncompatibleResultError
214
+ return tuple(sorted(self.entries.values(), key=lambda item: item.relative))
215
+
216
+
217
+ async def _walk_rows(
218
+ filesystem: AsyncFileSystem,
219
+ root: _ManifestEntry,
220
+ ) -> _Manifest:
221
+ builder = _ManifestBuilder(root)
222
+
223
+ def accept(row: _ListedRow) -> None:
224
+ builder.accept(
225
+ _walk_row(
226
+ root.path,
227
+ (row.root, row.directories, row.files),
228
+ entry_capacity=builder.capacity,
229
+ )
230
+ )
231
+
232
+ try:
233
+ await _walk(
234
+ filesystem,
235
+ root.path,
236
+ accept=accept,
237
+ capacity=lambda: builder.capacity,
238
+ )
239
+ except _IncompatibleListingError as error:
240
+ raise _IncompatibleResultError from error
241
+ except _ListingLimitError as error:
242
+ raise _EntryLimitError from error
243
+ return builder.finish()
244
+
245
+
246
+ def _relative_path(root: str, path: str) -> str:
247
+ relative = _lexical_relative(root, path)
248
+ if relative is None:
249
+ raise _IncompatibleResultError
250
+ return relative
251
+
252
+
253
+ async def _manifest(
254
+ filesystem: AsyncFileSystem,
255
+ path: str,
256
+ source_info: object,
257
+ ) -> _Manifest:
258
+ if not isinstance(source_info, Mapping):
259
+ raise _IncompatibleResultError
260
+ reported_path = source_info.get("name")
261
+ if type(reported_path) is not str or not _same_lexical_path(reported_path, path):
262
+ raise _IncompatibleResultError
263
+ root_entry = _entry("", path, source_info, expected_kind="directory")
264
+ return await _walk_rows(filesystem, root_entry)