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/__init__.py +24 -0
- fss_cli/_accounts.py +29 -0
- fss_cli/_app.py +663 -0
- fss_cli/_cat.py +391 -0
- fss_cli/_command.py +368 -0
- fss_cli/_concurrent.py +181 -0
- fss_cli/_content.py +94 -0
- fss_cli/_copy_file.py +65 -0
- fss_cli/_cp.py +740 -0
- fss_cli/_diagnostics.py +12 -0
- fss_cli/_du.py +90 -0
- fss_cli/_find.py +109 -0
- fss_cli/_head_tail.py +128 -0
- fss_cli/_info.py +179 -0
- fss_cli/_listing.py +289 -0
- fss_cli/_logging.py +72 -0
- fss_cli/_ls.py +435 -0
- fss_cli/_manifest.py +264 -0
- fss_cli/_metadata.py +70 -0
- fss_cli/_mkdir.py +97 -0
- fss_cli/_mv.py +146 -0
- fss_cli/_path.py +62 -0
- fss_cli/_post_copy.py +191 -0
- fss_cli/_progress.py +132 -0
- fss_cli/_recursive_cp.py +908 -0
- fss_cli/_recursive_rm.py +335 -0
- fss_cli/_results.py +115 -0
- fss_cli/_rm.py +149 -0
- fss_cli/_rmdir.py +148 -0
- fss_cli/_size.py +95 -0
- fss_cli/_sources.py +178 -0
- fss_cli/_stat.py +196 -0
- fss_cli/_test.py +59 -0
- fss_cli/_tree.py +208 -0
- fss_cli/_unlink.py +90 -0
- fss_cli/_walk.py +141 -0
- fss_cli-0.11.0.dist-info/METADATA +144 -0
- fss_cli-0.11.0.dist-info/RECORD +40 -0
- fss_cli-0.11.0.dist-info/WHEEL +4 -0
- fss_cli-0.11.0.dist-info/licenses/LICENSE +661 -0
fss_cli/_cat.py
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
"""Binary execution for typed mapped-file ``cat``."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import sys
|
|
7
|
+
import tempfile
|
|
8
|
+
from collections.abc import Mapping
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
from typing import TYPE_CHECKING, BinaryIO
|
|
12
|
+
|
|
13
|
+
import typer
|
|
14
|
+
|
|
15
|
+
from . import _copy_file
|
|
16
|
+
from ._command import (
|
|
17
|
+
_binary_stdout,
|
|
18
|
+
_BinaryWriter,
|
|
19
|
+
_CommandFailureError,
|
|
20
|
+
_drain_current_operation,
|
|
21
|
+
_Failure,
|
|
22
|
+
_MappedOperand,
|
|
23
|
+
_render_failure,
|
|
24
|
+
_render_output_failure,
|
|
25
|
+
_run_mapped_command,
|
|
26
|
+
_write_binary,
|
|
27
|
+
)
|
|
28
|
+
from ._diagnostics import _render_diagnostic_prefix, _render_diagnostic_value
|
|
29
|
+
|
|
30
|
+
if TYPE_CHECKING:
|
|
31
|
+
from fsspec.asyn import AsyncFileSystem
|
|
32
|
+
|
|
33
|
+
from ._app import AsyncFilesystemSource
|
|
34
|
+
|
|
35
|
+
_OUTPUT_CHUNK = 1 << 16
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class _StdinOperand:
|
|
40
|
+
spelling: str = "-"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_CatOperand = _MappedOperand | _StdinOperand
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@dataclass(frozen=True)
|
|
47
|
+
class _StagingFailure:
|
|
48
|
+
operand: _CatOperand
|
|
49
|
+
error: Exception
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def _binary_stdin() -> BinaryIO:
|
|
53
|
+
buffer = getattr(sys.stdin, "buffer", None)
|
|
54
|
+
if buffer is None:
|
|
55
|
+
message = "stdin has no binary buffer"
|
|
56
|
+
raise OSError(message)
|
|
57
|
+
return buffer
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _render_staging_failure(
|
|
61
|
+
command: str,
|
|
62
|
+
operand: _CatOperand,
|
|
63
|
+
error: Exception,
|
|
64
|
+
) -> None:
|
|
65
|
+
prefix = _render_diagnostic_prefix(command)
|
|
66
|
+
rendered_operand = _render_diagnostic_value(operand.spelling)
|
|
67
|
+
rendered_class = _render_diagnostic_value(type(error).__name__)
|
|
68
|
+
rendered_message = _render_diagnostic_value(str(error))
|
|
69
|
+
typer.echo(
|
|
70
|
+
f"{prefix} {rendered_operand}: staging failure ({rendered_class}): {rendered_message}",
|
|
71
|
+
err=True,
|
|
72
|
+
color=True,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _render_operand_failure(command: str, failure: _Failure | _StagingFailure) -> None:
|
|
77
|
+
if isinstance(failure, _StagingFailure):
|
|
78
|
+
_render_staging_failure(command, failure.operand, failure.error)
|
|
79
|
+
return
|
|
80
|
+
_render_failure(command, failure)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
@dataclass
|
|
84
|
+
class _CatOwnership:
|
|
85
|
+
pending_temporaries: set[str]
|
|
86
|
+
temporary_operands: dict[str, _MappedOperand]
|
|
87
|
+
pending_descriptors: dict[int, str]
|
|
88
|
+
|
|
89
|
+
def add(self, descriptor: int, temporary: str, operand: _MappedOperand) -> None:
|
|
90
|
+
self.pending_temporaries.add(temporary)
|
|
91
|
+
self.temporary_operands[temporary] = operand
|
|
92
|
+
self.pending_descriptors[descriptor] = temporary
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def _close_descriptor(
|
|
96
|
+
ownership: _CatOwnership,
|
|
97
|
+
descriptor: int,
|
|
98
|
+
) -> Exception | None:
|
|
99
|
+
try:
|
|
100
|
+
os.close(descriptor)
|
|
101
|
+
except Exception as error: # noqa: BLE001 - descriptor ownership boundary.
|
|
102
|
+
return error
|
|
103
|
+
ownership.pending_descriptors.pop(descriptor)
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _remove_owned_temporary(
|
|
108
|
+
ownership: _CatOwnership,
|
|
109
|
+
temporary: str,
|
|
110
|
+
) -> Exception | None:
|
|
111
|
+
if temporary not in ownership.pending_temporaries:
|
|
112
|
+
return None
|
|
113
|
+
if temporary in ownership.pending_descriptors.values():
|
|
114
|
+
message = "temporary descriptor remains open"
|
|
115
|
+
return OSError(message)
|
|
116
|
+
cleanup_error = _copy_file.remove_staging(temporary)
|
|
117
|
+
if cleanup_error is None:
|
|
118
|
+
ownership.pending_temporaries.remove(temporary)
|
|
119
|
+
ownership.temporary_operands.pop(temporary)
|
|
120
|
+
return cleanup_error
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _sweep_ownership(ownership: _CatOwnership) -> dict[str, Exception]:
|
|
124
|
+
errors: dict[str, Exception] = {}
|
|
125
|
+
for descriptor, temporary in tuple(ownership.pending_descriptors.items()):
|
|
126
|
+
close_error = _close_descriptor(ownership, descriptor)
|
|
127
|
+
if close_error is not None:
|
|
128
|
+
errors[temporary] = close_error
|
|
129
|
+
for temporary in tuple(ownership.pending_temporaries):
|
|
130
|
+
cleanup_error = _remove_owned_temporary(ownership, temporary)
|
|
131
|
+
if cleanup_error is not None:
|
|
132
|
+
errors.setdefault(temporary, cleanup_error)
|
|
133
|
+
else:
|
|
134
|
+
errors.pop(temporary, None)
|
|
135
|
+
return errors
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _failure_after_temporary(
|
|
139
|
+
ownership: _CatOwnership,
|
|
140
|
+
operand: _MappedOperand,
|
|
141
|
+
temporary: str,
|
|
142
|
+
error: Exception,
|
|
143
|
+
*,
|
|
144
|
+
staging: bool,
|
|
145
|
+
) -> _StagingFailure | _Failure:
|
|
146
|
+
cleanup_error = _remove_owned_temporary(ownership, temporary)
|
|
147
|
+
if cleanup_error is not None:
|
|
148
|
+
if staging and temporary in ownership.pending_descriptors.values():
|
|
149
|
+
return _StagingFailure(operand, error)
|
|
150
|
+
return _StagingFailure(operand, cleanup_error)
|
|
151
|
+
if staging:
|
|
152
|
+
return _StagingFailure(operand, error)
|
|
153
|
+
return _Failure(operand, backend_error=error)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
async def _stage_operand(
|
|
157
|
+
operand: _MappedOperand,
|
|
158
|
+
filesystem: AsyncFileSystem,
|
|
159
|
+
ownership: _CatOwnership,
|
|
160
|
+
) -> str | _Failure | _StagingFailure:
|
|
161
|
+
# fsspec's native async API intentionally exposes underscore coroutines.
|
|
162
|
+
try:
|
|
163
|
+
info = await _drain_current_operation(filesystem._info(operand.path))
|
|
164
|
+
except Exception as error: # noqa: BLE001 - classify awaited backend failure.
|
|
165
|
+
return _Failure(operand, backend_error=error)
|
|
166
|
+
|
|
167
|
+
if not isinstance(info, Mapping) or info.get("type") != "file":
|
|
168
|
+
return _Failure(operand)
|
|
169
|
+
|
|
170
|
+
try:
|
|
171
|
+
descriptor, temporary = tempfile.mkstemp(prefix="fss-cli-cat-")
|
|
172
|
+
except Exception as error: # noqa: BLE001 - local staging creation boundary.
|
|
173
|
+
return _StagingFailure(operand, error)
|
|
174
|
+
|
|
175
|
+
ownership.add(descriptor, temporary, operand)
|
|
176
|
+
close_error = _close_descriptor(ownership, descriptor)
|
|
177
|
+
if close_error is not None:
|
|
178
|
+
_sweep_ownership(ownership)
|
|
179
|
+
return _failure_after_temporary(
|
|
180
|
+
ownership,
|
|
181
|
+
operand,
|
|
182
|
+
temporary,
|
|
183
|
+
close_error,
|
|
184
|
+
staging=True,
|
|
185
|
+
)
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
await _drain_current_operation(filesystem._get_file(operand.path, temporary))
|
|
189
|
+
except Exception as error: # noqa: BLE001 - classify awaited backend failure.
|
|
190
|
+
return _failure_after_temporary(
|
|
191
|
+
ownership,
|
|
192
|
+
operand,
|
|
193
|
+
temporary,
|
|
194
|
+
error,
|
|
195
|
+
staging=False,
|
|
196
|
+
)
|
|
197
|
+
except BaseException:
|
|
198
|
+
_remove_owned_temporary(ownership, temporary)
|
|
199
|
+
raise
|
|
200
|
+
|
|
201
|
+
return temporary
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
@dataclass(frozen=True)
|
|
205
|
+
class _ForwardResult:
|
|
206
|
+
staging_error: Exception | None = None
|
|
207
|
+
output_error: Exception | None = None
|
|
208
|
+
wrote_output: bool = False
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _emit_handle(handle: BinaryIO, *, read_is_output: bool) -> _ForwardResult:
|
|
212
|
+
emitted = False
|
|
213
|
+
stdout: _BinaryWriter | None = None
|
|
214
|
+
while True:
|
|
215
|
+
try:
|
|
216
|
+
chunk = handle.read(_OUTPUT_CHUNK)
|
|
217
|
+
except Exception as error: # noqa: BLE001 - local staging/stdin read boundary.
|
|
218
|
+
if read_is_output and emitted:
|
|
219
|
+
return _ForwardResult(output_error=error, wrote_output=True)
|
|
220
|
+
return _ForwardResult(staging_error=error, wrote_output=emitted)
|
|
221
|
+
if not chunk:
|
|
222
|
+
break
|
|
223
|
+
if stdout is None:
|
|
224
|
+
try:
|
|
225
|
+
stdout = _binary_stdout()
|
|
226
|
+
except Exception as error: # noqa: BLE001 - stdout boundary.
|
|
227
|
+
return _ForwardResult(output_error=error, wrote_output=emitted)
|
|
228
|
+
try:
|
|
229
|
+
_write_binary(stdout, chunk)
|
|
230
|
+
except Exception as error: # noqa: BLE001 - stdout boundary.
|
|
231
|
+
return _ForwardResult(output_error=error, wrote_output=emitted)
|
|
232
|
+
emitted = True
|
|
233
|
+
if stdout is not None:
|
|
234
|
+
try:
|
|
235
|
+
stdout.flush()
|
|
236
|
+
except Exception as error: # noqa: BLE001 - stdout boundary.
|
|
237
|
+
return _ForwardResult(output_error=error, wrote_output=emitted)
|
|
238
|
+
return _ForwardResult(wrote_output=emitted)
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _forward_stdin() -> _ForwardResult:
|
|
242
|
+
try:
|
|
243
|
+
handle = _binary_stdin()
|
|
244
|
+
except Exception as error: # noqa: BLE001 - stdin open boundary.
|
|
245
|
+
return _ForwardResult(staging_error=error)
|
|
246
|
+
return _emit_handle(handle, read_is_output=False)
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _forward_temporary(temporary: str) -> _ForwardResult:
|
|
250
|
+
"""Stream one staging handle straight to stdout."""
|
|
251
|
+
try:
|
|
252
|
+
handle = Path(temporary).open("rb") # noqa: SIM115
|
|
253
|
+
except Exception as error: # noqa: BLE001 - local staging open boundary.
|
|
254
|
+
return _ForwardResult(staging_error=error)
|
|
255
|
+
|
|
256
|
+
forwarded = _ForwardResult()
|
|
257
|
+
try:
|
|
258
|
+
forwarded = _emit_handle(handle, read_is_output=True)
|
|
259
|
+
finally:
|
|
260
|
+
try:
|
|
261
|
+
handle.close()
|
|
262
|
+
except Exception as error: # noqa: BLE001 - local staging close boundary.
|
|
263
|
+
if forwarded.staging_error is None and forwarded.output_error is None:
|
|
264
|
+
if forwarded.wrote_output:
|
|
265
|
+
forwarded = _ForwardResult(
|
|
266
|
+
output_error=error,
|
|
267
|
+
wrote_output=True,
|
|
268
|
+
)
|
|
269
|
+
else:
|
|
270
|
+
forwarded = _ForwardResult(staging_error=error)
|
|
271
|
+
|
|
272
|
+
return forwarded
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
@dataclass
|
|
276
|
+
class _CatProgress:
|
|
277
|
+
failures: list[_Failure | _StagingFailure]
|
|
278
|
+
output_error: Exception | None = None
|
|
279
|
+
staging_cleanup_error: Exception | None = None
|
|
280
|
+
|
|
281
|
+
@property
|
|
282
|
+
def succeeded(self) -> bool:
|
|
283
|
+
return not self.failures and self.output_error is None and self.staging_cleanup_error is None
|
|
284
|
+
|
|
285
|
+
def command_error(self) -> Exception | None:
|
|
286
|
+
for failure in self.failures:
|
|
287
|
+
if isinstance(failure, _StagingFailure):
|
|
288
|
+
return failure.error
|
|
289
|
+
if failure.backend_error is not None:
|
|
290
|
+
return failure.backend_error
|
|
291
|
+
if self.output_error is not None:
|
|
292
|
+
return self.output_error
|
|
293
|
+
return self.staging_cleanup_error
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def _apply_forward_result(
|
|
297
|
+
command: str,
|
|
298
|
+
operand: _CatOperand,
|
|
299
|
+
forwarded: _ForwardResult,
|
|
300
|
+
progress: _CatProgress,
|
|
301
|
+
) -> None:
|
|
302
|
+
if forwarded.staging_error is not None:
|
|
303
|
+
failure = _StagingFailure(operand, forwarded.staging_error)
|
|
304
|
+
_render_operand_failure(command, failure)
|
|
305
|
+
progress.failures.append(failure)
|
|
306
|
+
return
|
|
307
|
+
if forwarded.output_error is not None:
|
|
308
|
+
progress.output_error = forwarded.output_error
|
|
309
|
+
if not isinstance(forwarded.output_error, BrokenPipeError):
|
|
310
|
+
_render_output_failure(command, forwarded.output_error)
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
async def _emit_mapped_operand(
|
|
314
|
+
command: str,
|
|
315
|
+
operand: _MappedOperand,
|
|
316
|
+
filesystem: AsyncFileSystem,
|
|
317
|
+
progress: _CatProgress,
|
|
318
|
+
ownership: _CatOwnership,
|
|
319
|
+
) -> None:
|
|
320
|
+
staged = await _stage_operand(operand, filesystem, ownership)
|
|
321
|
+
if not isinstance(staged, str):
|
|
322
|
+
_render_operand_failure(command, staged)
|
|
323
|
+
progress.failures.append(staged)
|
|
324
|
+
return
|
|
325
|
+
temporary = staged
|
|
326
|
+
try:
|
|
327
|
+
_apply_forward_result(
|
|
328
|
+
command,
|
|
329
|
+
operand,
|
|
330
|
+
_forward_temporary(temporary),
|
|
331
|
+
progress,
|
|
332
|
+
)
|
|
333
|
+
finally:
|
|
334
|
+
cleanup_error = _remove_owned_temporary(ownership, temporary)
|
|
335
|
+
if cleanup_error is not None:
|
|
336
|
+
progress.staging_cleanup_error = cleanup_error
|
|
337
|
+
_render_staging_failure(command, operand, cleanup_error)
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
async def _emit_operands(
|
|
341
|
+
command: str,
|
|
342
|
+
operands: tuple[_CatOperand, ...],
|
|
343
|
+
filesystems: Mapping[str, AsyncFileSystem],
|
|
344
|
+
progress: _CatProgress,
|
|
345
|
+
ownership: _CatOwnership,
|
|
346
|
+
) -> None:
|
|
347
|
+
for operand in operands:
|
|
348
|
+
if progress.output_error is not None:
|
|
349
|
+
return
|
|
350
|
+
if isinstance(operand, _StdinOperand):
|
|
351
|
+
_apply_forward_result(command, operand, _forward_stdin(), progress)
|
|
352
|
+
continue
|
|
353
|
+
await _emit_mapped_operand(
|
|
354
|
+
command,
|
|
355
|
+
operand,
|
|
356
|
+
filesystems[operand.name],
|
|
357
|
+
progress,
|
|
358
|
+
ownership,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
async def _run_cat(
|
|
363
|
+
command: str,
|
|
364
|
+
operands: tuple[_CatOperand, ...],
|
|
365
|
+
sources: Mapping[str, AsyncFilesystemSource],
|
|
366
|
+
) -> None:
|
|
367
|
+
mapped = tuple(operand for operand in operands if isinstance(operand, _MappedOperand))
|
|
368
|
+
|
|
369
|
+
async def execute(filesystems: Mapping[str, AsyncFileSystem]) -> None:
|
|
370
|
+
progress = _CatProgress(failures=[])
|
|
371
|
+
ownership = _CatOwnership(set(), {}, {})
|
|
372
|
+
try:
|
|
373
|
+
await _emit_operands(
|
|
374
|
+
command,
|
|
375
|
+
operands,
|
|
376
|
+
filesystems,
|
|
377
|
+
progress,
|
|
378
|
+
ownership,
|
|
379
|
+
)
|
|
380
|
+
finally:
|
|
381
|
+
for temporary, error in _sweep_ownership(ownership).items():
|
|
382
|
+
operand = ownership.temporary_operands[temporary]
|
|
383
|
+
_render_staging_failure(command, operand, error)
|
|
384
|
+
progress.staging_cleanup_error = error
|
|
385
|
+
if not progress.succeeded:
|
|
386
|
+
raise _CommandFailureError(
|
|
387
|
+
error=progress.command_error(),
|
|
388
|
+
render=False,
|
|
389
|
+
)
|
|
390
|
+
|
|
391
|
+
await _run_mapped_command(command, mapped, sources, execute)
|