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.
@@ -0,0 +1,12 @@
1
+ """Shared rendering for stable command diagnostics."""
2
+
3
+ _ESCAPES = {code: f"\\x{code:02x}" for code in (*range(0x20), 0x7F)}
4
+ _ESCAPES[ord("\\")] = "\\\\"
5
+
6
+
7
+ def _render_diagnostic_value(value: str) -> str:
8
+ return value.translate(_ESCAPES)
9
+
10
+
11
+ def _render_diagnostic_prefix(command: str) -> str:
12
+ return f"{_render_diagnostic_value(command)}:"
fss_cli/_du.py ADDED
@@ -0,0 +1,90 @@
1
+ """Disk-usage execution for the central ``du`` callback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import TYPE_CHECKING
7
+
8
+ from ._command import (
9
+ _collate,
10
+ _Failure,
11
+ _MappedOperand,
12
+ _run_single_operand_text,
13
+ _valid_size,
14
+ )
15
+ from ._listing import format_size
16
+ from ._metadata import valid_display_text
17
+ from ._walk import _IncompatibleListingError, _ListedRow, _walk
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Mapping
21
+
22
+ from fsspec.asyn import AsyncFileSystem
23
+
24
+ from ._app import AsyncFilesystemSource
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class _DuRequest:
29
+ summarize: bool
30
+ human_readable: bool
31
+ operand: _MappedOperand
32
+
33
+
34
+ def _file_sizes(rows: list[_ListedRow]) -> dict[str, int] | None:
35
+ """Collect each listed file's size by reported name, or ``None`` if invalid.
36
+
37
+ Like fsspec's ``_du``, every non-directory entry counts and a name listed
38
+ twice counts once.
39
+ """
40
+ sizes: dict[str, int] = {}
41
+ try:
42
+ for row in rows:
43
+ for info in row.files.values():
44
+ path = info.get("name")
45
+ size = info.get("size")
46
+ if type(path) is not str or not valid_display_text(path) or not _valid_size(size):
47
+ return None
48
+ sizes[path] = size
49
+ except Exception: # noqa: BLE001 - fail closed on hostile mapping behavior.
50
+ return None
51
+ return sizes
52
+
53
+
54
+ def _render_sizes(request: _DuRequest, sizes: Mapping[str, int]) -> str:
55
+ if request.summarize:
56
+ size = format_size(sum(sizes.values()), human_readable=request.human_readable)
57
+ return f"{size}\t{request.operand.path}\n"
58
+ return "".join(
59
+ f"{format_size(size, human_readable=request.human_readable)}\t{path}\n"
60
+ for path, size in sorted(sizes.items(), key=lambda entry: _collate(entry[0]))
61
+ )
62
+
63
+
64
+ async def _measure(
65
+ request: _DuRequest,
66
+ filesystem: AsyncFileSystem,
67
+ ) -> str | _Failure:
68
+ try:
69
+ rows = await _walk(filesystem, request.operand.path)
70
+ except _IncompatibleListingError:
71
+ return _Failure(request.operand)
72
+ except Exception as error: # noqa: BLE001 - classify awaited backend failure.
73
+ return _Failure(request.operand, backend_error=error)
74
+ sizes = _file_sizes(rows)
75
+ if sizes is None:
76
+ return _Failure(request.operand)
77
+ return _render_sizes(request, sizes)
78
+
79
+
80
+ async def _run_du(
81
+ command: str,
82
+ request: _DuRequest,
83
+ sources: Mapping[str, AsyncFilesystemSource],
84
+ ) -> None:
85
+ await _run_single_operand_text(
86
+ command,
87
+ request.operand,
88
+ sources,
89
+ lambda filesystem: _measure(request, filesystem),
90
+ )
fss_cli/_find.py ADDED
@@ -0,0 +1,109 @@
1
+ """Recursive-find execution for the central ``find`` callback."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING, Literal, TypeGuard, cast
8
+
9
+ from ._command import (
10
+ _call,
11
+ _collate,
12
+ _Failure,
13
+ _MappedOperand,
14
+ _run_single_operand_text,
15
+ )
16
+ from ._metadata import valid_display_text
17
+ from ._walk import _IncompatibleListingError, _ListedRow, _walk
18
+
19
+ if TYPE_CHECKING:
20
+ from fsspec.asyn import AsyncFileSystem
21
+
22
+ from ._app import AsyncFilesystemSource
23
+
24
+
25
+ @dataclass(frozen=True)
26
+ class _FindRequest:
27
+ maxdepth: int | None
28
+ kind: Literal["f", "d"]
29
+ operand: _MappedOperand
30
+
31
+
32
+ def _render_paths(request: _FindRequest, paths: list[str]) -> str:
33
+ if request.maxdepth == 0:
34
+ root = request.operand.path.rstrip("/")
35
+ paths = [path for path in paths if path.rstrip("/") == root]
36
+ paths.sort(key=_collate)
37
+ return "".join(f"{path}\n" for path in dict.fromkeys(paths))
38
+
39
+
40
+ def _selected_paths(
41
+ request: _FindRequest,
42
+ root_info: object,
43
+ rows: list[_ListedRow],
44
+ ) -> list[str] | None:
45
+ """Select displayable file or directory paths, or ``None`` if invalid.
46
+
47
+ File entries are every non-directory row entry, as fsspec's ``_find``
48
+ reports them; ``--type d`` reports the operand itself when it is a
49
+ directory plus every listed directory.
50
+ """
51
+ infos: list[Mapping[object, object]] = []
52
+ if request.kind == "d":
53
+ if not isinstance(root_info, Mapping):
54
+ return None
55
+ infos.append(cast("Mapping[object, object]", root_info))
56
+ for row in rows:
57
+ if request.kind == "d":
58
+ infos.extend(row.directories.values())
59
+ infos.extend(row.files.values())
60
+ paths: list[str] = []
61
+ try:
62
+ for info in infos:
63
+ path = info.get("name")
64
+ kind = info.get("type")
65
+ if not _valid_path(path) or type(kind) is not str:
66
+ return None
67
+ if (kind == "directory") == (request.kind == "d"):
68
+ paths.append(path)
69
+ except Exception: # noqa: BLE001 - fail closed on hostile mapping consumption.
70
+ return None
71
+ return paths
72
+
73
+
74
+ def _valid_path(path: object) -> TypeGuard[str]:
75
+ return type(path) is str and valid_display_text(path)
76
+
77
+
78
+ async def _search(
79
+ request: _FindRequest,
80
+ filesystem: AsyncFileSystem,
81
+ ) -> str | _Failure:
82
+ try:
83
+ root_info = await _call(filesystem, "_info", request.operand.path) if request.kind == "d" else None
84
+ rows = await _walk(
85
+ filesystem,
86
+ request.operand.path,
87
+ maxdepth=1 if request.maxdepth == 0 else request.maxdepth,
88
+ )
89
+ except _IncompatibleListingError:
90
+ return _Failure(request.operand)
91
+ except Exception as error: # noqa: BLE001 - classify awaited backend failure.
92
+ return _Failure(request.operand, backend_error=error)
93
+ paths = _selected_paths(request, root_info, rows)
94
+ if paths is None:
95
+ return _Failure(request.operand)
96
+ return _render_paths(request, paths)
97
+
98
+
99
+ async def _run_find(
100
+ command: str,
101
+ request: _FindRequest,
102
+ sources: Mapping[str, AsyncFilesystemSource],
103
+ ) -> None:
104
+ await _run_single_operand_text(
105
+ command,
106
+ request.operand,
107
+ sources,
108
+ lambda filesystem: _search(request, filesystem),
109
+ )
fss_cli/_head_tail.py ADDED
@@ -0,0 +1,128 @@
1
+ """Typed ``head`` and ``tail`` runtime."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from dataclasses import dataclass
7
+ from typing import TYPE_CHECKING
8
+
9
+ from ._command import (
10
+ _binary_stdout,
11
+ _CommandFailureError,
12
+ _drain_current_operation,
13
+ _MappedOperand,
14
+ _run_mapped_command,
15
+ _valid_size,
16
+ _write_binary,
17
+ )
18
+
19
+ if TYPE_CHECKING:
20
+ from collections.abc import Awaitable, Callable
21
+
22
+ from fsspec.asyn import AsyncFileSystem
23
+
24
+ from ._app import AsyncFilesystemSource
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class _ByteRangeRequest:
29
+ count: int
30
+ operand: _MappedOperand
31
+
32
+
33
+ async def _read_head(
34
+ request: _ByteRangeRequest,
35
+ filesystem: AsyncFileSystem,
36
+ ) -> bytes:
37
+ if request.count == 0:
38
+ try:
39
+ info = await _drain_current_operation(filesystem._info(request.operand.path))
40
+ except Exception as error:
41
+ raise _CommandFailureError(request.operand, error) from error
42
+ if _is_file(request.operand, info):
43
+ return b""
44
+ try:
45
+ result = await _drain_current_operation(filesystem._cat_file(request.operand.path, start=0, end=request.count))
46
+ except Exception as error:
47
+ raise _CommandFailureError(request.operand, error) from error
48
+ if type(result) is not bytes or len(result) > request.count:
49
+ raise _CommandFailureError(request.operand)
50
+ return result
51
+
52
+
53
+ def _info_field(operand: _MappedOperand, info: object, key: str) -> object:
54
+ if not isinstance(info, Mapping):
55
+ raise _CommandFailureError(operand)
56
+ try:
57
+ return info.get(key)
58
+ except Exception: # noqa: BLE001 - hostile metadata mapping boundary.
59
+ raise _CommandFailureError(operand) from None
60
+
61
+
62
+ def _size_from_info(operand: _MappedOperand, info: object) -> int:
63
+ size = _info_field(operand, info, "size")
64
+ if not _valid_size(size):
65
+ raise _CommandFailureError(operand)
66
+ return size
67
+
68
+
69
+ def _is_file(operand: _MappedOperand, info: object) -> bool:
70
+ """Return whether ``info`` proves a regular file, so a zero read needs no bytes.
71
+
72
+ Any other kind falls back to the bounded read, so the backend still owns
73
+ the diagnostic for directories and other non-file operands.
74
+ """
75
+ kind = _info_field(operand, info, "type")
76
+ return type(kind) is str and kind == "file"
77
+
78
+
79
+ async def _read_tail(
80
+ request: _ByteRangeRequest,
81
+ filesystem: AsyncFileSystem,
82
+ ) -> bytes:
83
+ try:
84
+ info = await _drain_current_operation(filesystem._info(request.operand.path))
85
+ except Exception as error:
86
+ raise _CommandFailureError(request.operand, error) from error
87
+ _size_from_info(request.operand, info)
88
+ if request.count == 0 and _is_file(request.operand, info):
89
+ return b""
90
+ try:
91
+ result = await _drain_current_operation(
92
+ filesystem._cat_file(request.operand.path, start=-request.count, end=None)
93
+ )
94
+ except Exception as error:
95
+ raise _CommandFailureError(request.operand, error) from error
96
+ if type(result) is not bytes or len(result) > request.count:
97
+ raise _CommandFailureError(request.operand)
98
+ return result
99
+
100
+
101
+ def _emit(payload: bytes) -> None:
102
+ if not payload:
103
+ return
104
+ stdout = _binary_stdout()
105
+ _write_binary(stdout, payload)
106
+ stdout.flush()
107
+
108
+
109
+ async def _run_byte_range(
110
+ command: str,
111
+ count: int,
112
+ operand: _MappedOperand,
113
+ sources: Mapping[str, AsyncFilesystemSource],
114
+ operation: Callable[
115
+ [_ByteRangeRequest, AsyncFileSystem],
116
+ Awaitable[bytes],
117
+ ],
118
+ ) -> None:
119
+ request = _ByteRangeRequest(count, operand)
120
+
121
+ async def execute(filesystems: Mapping[str, AsyncFileSystem]) -> None:
122
+ payload = await operation(request, filesystems[operand.name])
123
+ try:
124
+ _emit(payload)
125
+ except Exception as error:
126
+ raise _CommandFailureError(error=error) from error
127
+
128
+ await _run_mapped_command(command, (operand,), sources, execute)
fss_cli/_info.py ADDED
@@ -0,0 +1,179 @@
1
+ """Normalized metadata execution for typed ``info``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pprint
6
+ from collections.abc import Mapping
7
+ from dataclasses import dataclass
8
+ from typing import TYPE_CHECKING, cast
9
+
10
+ from ._command import (
11
+ _binary_stdout,
12
+ _CommandFailureError,
13
+ _drain_current_operation,
14
+ _MappedOperand,
15
+ _run_mapped_command,
16
+ _write_binary,
17
+ )
18
+ from ._listing import ListingRow, to_listing
19
+
20
+ if TYPE_CHECKING:
21
+ from fsspec.asyn import AsyncFileSystem
22
+
23
+ from ._app import AsyncFilesystemSource
24
+
25
+ _PRETTY_WIDTH = 80
26
+
27
+
28
+ @dataclass(frozen=True, order=True)
29
+ class _StablePresentation:
30
+ sort_type: str
31
+ text: str
32
+
33
+ def __repr__(self) -> str:
34
+ return self.text
35
+
36
+
37
+ def _canonical_value(value: object, active: set[int]) -> object:
38
+ if not isinstance(value, (Mapping, list, tuple, set, frozenset)):
39
+ return value
40
+
41
+ identity = id(value)
42
+ if identity in active:
43
+ message = "recursive metadata containers are not supported"
44
+ raise ValueError(message)
45
+ active.add(identity)
46
+ try:
47
+ if isinstance(value, Mapping):
48
+ canonical: object = _canonical_mapping(cast("Mapping[object, object]", value), active)
49
+ elif isinstance(value, list):
50
+ canonical = [_canonical_value(item, active) for item in value]
51
+ elif isinstance(value, tuple):
52
+ canonical = tuple(_canonical_value(item, active) for item in value)
53
+ elif isinstance(value, set):
54
+ canonical = _canonical_set(cast("set[object]", value), active, frozen=False)
55
+ else:
56
+ canonical = _canonical_set(value, active, frozen=True)
57
+ return canonical
58
+ finally:
59
+ active.remove(identity)
60
+
61
+
62
+ def _canonical_mapping(
63
+ value: Mapping[object, object],
64
+ active: set[int],
65
+ ) -> dict[object, object]:
66
+ source_length = len(value)
67
+ entries: list[tuple[object, object]] = []
68
+ spellings: set[str] = set()
69
+ for key in value:
70
+ item = value[key]
71
+ canonical_key = _canonical_value(key, active)
72
+ hash(canonical_key)
73
+ spelling = _pretty(canonical_key)
74
+ if spelling in spellings:
75
+ message = "distinct mapping keys have the same presentation"
76
+ raise ValueError(message)
77
+ spellings.add(spelling)
78
+ entries.append(
79
+ (
80
+ _StablePresentation(
81
+ f"{type(key).__module__}.{type(key).__qualname__}",
82
+ spelling,
83
+ ),
84
+ _canonical_value(item, active),
85
+ )
86
+ )
87
+
88
+ if len(entries) != source_length:
89
+ message = "mapping iteration does not match its reported length"
90
+ raise ValueError(message)
91
+ canonical = dict(entries)
92
+ if len(canonical) != source_length:
93
+ message = "canonical mapping keys are not distinct"
94
+ raise ValueError(message)
95
+ return canonical
96
+
97
+
98
+ def _canonical_set(
99
+ value: set[object] | frozenset[object],
100
+ active: set[int],
101
+ *,
102
+ frozen: bool,
103
+ ) -> _StablePresentation:
104
+ members = sorted(_pretty(_canonical_value(item, active)) for item in value)
105
+ if frozen:
106
+ spelling = "frozenset()" if not members else f"frozenset({{{', '.join(members)}}})"
107
+ else:
108
+ spelling = "set()" if not members else f"{{{', '.join(members)}}}"
109
+ return _StablePresentation(f"{type(value).__module__}.{type(value).__qualname__}", spelling)
110
+
111
+
112
+ def _pretty(value: object) -> str:
113
+ return pprint.pformat(value, width=_PRETTY_WIDTH, sort_dicts=True)
114
+
115
+
116
+ def _render_info(row: ListingRow) -> bytes:
117
+ values = {
118
+ "name": row.name,
119
+ "kind": row.kind,
120
+ "size": row.size,
121
+ "mtime": row.mtime,
122
+ "mode": row.mode,
123
+ "nlink": row.nlink,
124
+ "owner": row.owner,
125
+ "group": row.group,
126
+ "link_target": row.link_target,
127
+ "extra": _canonical_value(row.extra, set()),
128
+ }
129
+ rendered = _pretty(values)
130
+ return f"{rendered}\n".encode()
131
+
132
+
133
+ def _normalize_info(result: object) -> bytes | None:
134
+ if not isinstance(result, Mapping):
135
+ return None
136
+ try:
137
+ if any(type(key) is not str for key in result):
138
+ return None
139
+ row = to_listing(cast("Mapping[str, object]", result))
140
+ return _render_info(row)
141
+ except Exception: # noqa: BLE001 - malformed mapping or printable value.
142
+ return None
143
+
144
+
145
+ async def _read_info(
146
+ operand: _MappedOperand,
147
+ filesystem: AsyncFileSystem,
148
+ ) -> bytes:
149
+ try:
150
+ result = await _drain_current_operation(filesystem._info(operand.path))
151
+ except Exception as error:
152
+ raise _CommandFailureError(operand, error) from error
153
+ payload = _normalize_info(result)
154
+ if payload is None:
155
+ raise _CommandFailureError(operand)
156
+ return payload
157
+
158
+
159
+ async def _run_info(
160
+ command: str,
161
+ operand: _MappedOperand,
162
+ sources: Mapping[str, AsyncFilesystemSource],
163
+ ) -> None:
164
+ async def execute(filesystems: Mapping[str, AsyncFileSystem]) -> None:
165
+ payload = await _read_info(operand, filesystems[operand.name])
166
+ try:
167
+ stdout = _binary_stdout()
168
+ _write_binary(stdout, payload)
169
+ stdout.flush()
170
+ except Exception as error:
171
+ raise _CommandFailureError(error=error) from error
172
+
173
+ await _run_mapped_command(
174
+ command,
175
+ (operand,),
176
+ sources,
177
+ execute,
178
+ broken_pipe_exit_code=1,
179
+ )