fx-bin 2.2.2__tar.gz → 2.3.0__tar.gz
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.
- {fx_bin-2.2.2 → fx_bin-2.3.0}/PKG-INFO +1 -1
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/__init__.py +1 -1
- fx_bin-2.3.0/fx_bin/backup_utils.py +123 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/cli.py +32 -22
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/common.py +16 -7
- fx_bin-2.3.0/fx_bin/common_functional.py +415 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/errors.py +34 -6
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/files.py +2 -2
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/find_files.py +0 -1
- fx_bin-2.3.0/fx_bin/lib.py +80 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/py_fx_bin.py +0 -1
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/replace.py +31 -30
- fx_bin-2.3.0/fx_bin/replace_functional.py +415 -0
- fx_bin-2.3.0/fx_bin/shared_types.py +54 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/size.py +2 -2
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/today.py +9 -4
- {fx_bin-2.2.2 → fx_bin-2.3.0}/pyproject.toml +3 -1
- fx_bin-2.2.2/fx_bin/common_functional.py +0 -259
- fx_bin-2.2.2/fx_bin/lib.py +0 -42
- fx_bin-2.2.2/fx_bin/replace_functional.py +0 -304
- {fx_bin-2.2.2 → fx_bin-2.3.0}/README.md +0 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/backup.py +0 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/filter.py +0 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/realpath.py +0 -0
- {fx_bin-2.2.2 → fx_bin-2.3.0}/fx_bin/root.py +0 -0
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"""Shared backup utilities for file operations.
|
|
2
|
+
|
|
3
|
+
This module provides reusable backup functionality that can be used
|
|
4
|
+
across different modules (replace.py, replace_functional.py, etc.).
|
|
5
|
+
|
|
6
|
+
Functions follow functional patterns with IOResult for error handling.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import os
|
|
10
|
+
import shutil
|
|
11
|
+
import stat
|
|
12
|
+
|
|
13
|
+
from loguru import logger as L
|
|
14
|
+
from returns.io import IOResult
|
|
15
|
+
|
|
16
|
+
from fx_bin.errors import IOError as FxIOError
|
|
17
|
+
from fx_bin.shared_types import FileBackup
|
|
18
|
+
|
|
19
|
+
# Re-export for backward compatibility
|
|
20
|
+
__all__ = ["FileBackup", "create_backup", "restore_from_backup", "cleanup_backup"]
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def create_backup(filename: str) -> IOResult[FileBackup, FxIOError]:
|
|
24
|
+
"""Create a backup of the file.
|
|
25
|
+
|
|
26
|
+
Creates a backup copy of the specified file with preserved metadata.
|
|
27
|
+
The backup file is created with a ".backup" suffix.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
filename: Path to the file to backup
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
IOResult containing:
|
|
34
|
+
- Success: FileBackup object with backup metadata
|
|
35
|
+
- Failure: FxIOError if backup creation fails
|
|
36
|
+
|
|
37
|
+
Example:
|
|
38
|
+
>>> from fx_bin.lib import unsafe_ioresult_unwrap
|
|
39
|
+
>>> result = create_backup("/path/to/file.txt")
|
|
40
|
+
>>> backup = unsafe_ioresult_unwrap(result)
|
|
41
|
+
>>> print(backup.backup_path)
|
|
42
|
+
/path/to/file.txt.backup
|
|
43
|
+
"""
|
|
44
|
+
try:
|
|
45
|
+
# Get original file stats
|
|
46
|
+
original_stat = os.stat(filename)
|
|
47
|
+
original_mode = original_stat.st_mode
|
|
48
|
+
|
|
49
|
+
# Create backup path
|
|
50
|
+
backup_path = f"{filename}.backup"
|
|
51
|
+
|
|
52
|
+
# Copy file with metadata
|
|
53
|
+
shutil.copy2(filename, backup_path)
|
|
54
|
+
|
|
55
|
+
return IOResult.from_value(
|
|
56
|
+
FileBackup(
|
|
57
|
+
original_path=filename,
|
|
58
|
+
backup_path=backup_path,
|
|
59
|
+
original_mode=original_mode,
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
except Exception as e:
|
|
63
|
+
return IOResult.from_failure(FxIOError(f"Failed to create backup: {e}"))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def restore_from_backup(backup: FileBackup) -> IOResult[None, FxIOError]:
|
|
67
|
+
"""Restore original file from backup on failure.
|
|
68
|
+
|
|
69
|
+
Restores the original file from the backup copy and restores
|
|
70
|
+
the original file permissions. Gracefully handles missing backup
|
|
71
|
+
files (already cleaned up).
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
backup: FileBackup object containing backup metadata
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
IOResult containing:
|
|
78
|
+
- Success: None (restore succeeded or backup already cleaned up)
|
|
79
|
+
- Failure: FxIOError if restore operation fails
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
>>> backup = FileBackup(...)
|
|
83
|
+
>>> result = restore_from_backup(backup)
|
|
84
|
+
"""
|
|
85
|
+
try:
|
|
86
|
+
if os.path.exists(backup.backup_path):
|
|
87
|
+
# Restore the backup
|
|
88
|
+
shutil.move(backup.backup_path, backup.original_path)
|
|
89
|
+
# Restore permissions
|
|
90
|
+
os.chmod(backup.original_path, stat.S_IMODE(backup.original_mode))
|
|
91
|
+
return IOResult.from_value(None)
|
|
92
|
+
except Exception as e:
|
|
93
|
+
return IOResult.from_failure(FxIOError(f"Failed to restore from backup: {e}"))
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def cleanup_backup(backup: FileBackup) -> IOResult[None, FxIOError]:
|
|
97
|
+
"""Remove backup file after successful operation.
|
|
98
|
+
|
|
99
|
+
Cleans up the backup file after a successful operation.
|
|
100
|
+
Gracefully handles missing backup files (already cleaned up).
|
|
101
|
+
This is a non-critical operation - failures are logged but don't
|
|
102
|
+
fail the overall operation.
|
|
103
|
+
|
|
104
|
+
Args:
|
|
105
|
+
backup: FileBackup object containing backup metadata
|
|
106
|
+
|
|
107
|
+
Returns:
|
|
108
|
+
IOResult containing:
|
|
109
|
+
- Success: None (cleanup succeeded or backup already gone)
|
|
110
|
+
- Failure: Never fails - errors logged but returns Success
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
>>> backup = FileBackup(...)
|
|
114
|
+
>>> result = cleanup_backup(backup)
|
|
115
|
+
"""
|
|
116
|
+
try:
|
|
117
|
+
if os.path.exists(backup.backup_path):
|
|
118
|
+
os.unlink(backup.backup_path)
|
|
119
|
+
return IOResult.from_value(None)
|
|
120
|
+
except Exception as e:
|
|
121
|
+
# Non-critical error - log but don't fail
|
|
122
|
+
L.warning(f"Could not remove backup {backup.backup_path}: {e}")
|
|
123
|
+
return IOResult.from_value(None)
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
|
|
4
4
|
import click
|
|
5
5
|
import sys
|
|
6
|
-
from typing import List, Tuple, Optional
|
|
6
|
+
from typing import List, Tuple, Optional, Any
|
|
7
|
+
|
|
7
8
|
import importlib.metadata
|
|
8
9
|
|
|
9
10
|
|
|
@@ -41,7 +42,7 @@ COMMANDS_INFO: List[Tuple[str, str]] = [
|
|
|
41
42
|
)
|
|
42
43
|
@click.version_option(version=None, message=get_version_info())
|
|
43
44
|
@click.pass_context
|
|
44
|
-
def cli(ctx):
|
|
45
|
+
def cli(ctx: click.Context) -> None:
|
|
45
46
|
"""FX - A collection of file and text utilities.
|
|
46
47
|
|
|
47
48
|
Common commands:
|
|
@@ -60,7 +61,7 @@ def cli(ctx):
|
|
|
60
61
|
|
|
61
62
|
@cli.command()
|
|
62
63
|
@click.argument("paths", nargs=-1, type=click.Path(exists=True))
|
|
63
|
-
def files(paths):
|
|
64
|
+
def files(paths: Tuple[str, ...]) -> int:
|
|
64
65
|
"""Count files in directories.
|
|
65
66
|
|
|
66
67
|
Examples:
|
|
@@ -88,7 +89,7 @@ def files(paths):
|
|
|
88
89
|
|
|
89
90
|
@cli.command()
|
|
90
91
|
@click.argument("paths", nargs=-1, type=click.Path(exists=True))
|
|
91
|
-
def size(paths):
|
|
92
|
+
def size(paths: Tuple[str, ...]) -> int:
|
|
92
93
|
"""Analyze file and directory sizes.
|
|
93
94
|
|
|
94
95
|
Examples:
|
|
@@ -158,7 +159,9 @@ def _run_ff(
|
|
|
158
159
|
"--exclude build --exclude '*.log'"
|
|
159
160
|
),
|
|
160
161
|
)
|
|
161
|
-
def ff(
|
|
162
|
+
def ff(
|
|
163
|
+
keyword: str, first: bool, include_ignored: bool, excludes: Tuple[str, ...]
|
|
164
|
+
) -> int:
|
|
162
165
|
"""Find files whose names contain KEYWORD.
|
|
163
166
|
|
|
164
167
|
\b
|
|
@@ -193,7 +196,7 @@ def ff(keyword, first, include_ignored, excludes):
|
|
|
193
196
|
|
|
194
197
|
@cli.command()
|
|
195
198
|
@click.argument("keyword")
|
|
196
|
-
def fff(keyword):
|
|
199
|
+
def fff(keyword: str) -> int:
|
|
197
200
|
"""Find first file matching KEYWORD.
|
|
198
201
|
|
|
199
202
|
Alias for `fx ff KEYWORD --first`. Returns only the first match
|
|
@@ -248,15 +251,15 @@ def fff(keyword):
|
|
|
248
251
|
help="Limit the number of results returned",
|
|
249
252
|
)
|
|
250
253
|
def filter(
|
|
251
|
-
extension,
|
|
252
|
-
paths,
|
|
253
|
-
recursive,
|
|
254
|
-
sort_by,
|
|
255
|
-
reverse,
|
|
256
|
-
output_format,
|
|
257
|
-
show_path,
|
|
258
|
-
limit,
|
|
259
|
-
):
|
|
254
|
+
extension: str,
|
|
255
|
+
paths: Tuple[str, ...],
|
|
256
|
+
recursive: bool,
|
|
257
|
+
sort_by: Optional[str],
|
|
258
|
+
reverse: bool,
|
|
259
|
+
output_format: str,
|
|
260
|
+
show_path: bool,
|
|
261
|
+
limit: Optional[int],
|
|
262
|
+
) -> int:
|
|
260
263
|
"""Filter files by extension.
|
|
261
264
|
|
|
262
265
|
\b
|
|
@@ -322,7 +325,7 @@ def filter(
|
|
|
322
325
|
@click.argument("search_text")
|
|
323
326
|
@click.argument("replace_text")
|
|
324
327
|
@click.argument("filenames", nargs=-1, required=True)
|
|
325
|
-
def replace(search_text, replace_text, filenames):
|
|
328
|
+
def replace(search_text: str, replace_text: str, filenames: Tuple[str, ...]) -> int:
|
|
326
329
|
"""Replace text in files.
|
|
327
330
|
|
|
328
331
|
Examples:
|
|
@@ -389,7 +392,7 @@ def backup(
|
|
|
389
392
|
is_flag=True,
|
|
390
393
|
help="Output path suitable for cd command (no extra text)",
|
|
391
394
|
)
|
|
392
|
-
def root(output_for_cd):
|
|
395
|
+
def root(output_for_cd: bool) -> int:
|
|
393
396
|
"""Find Git project root directory.
|
|
394
397
|
|
|
395
398
|
Searches upward from current directory to find the first directory
|
|
@@ -500,7 +503,14 @@ def realpath(path: str) -> int:
|
|
|
500
503
|
@click.option(
|
|
501
504
|
"--no-exec", is_flag=True, help="Don't start new shell, just create directory"
|
|
502
505
|
)
|
|
503
|
-
def today(
|
|
506
|
+
def today(
|
|
507
|
+
output_for_cd: bool,
|
|
508
|
+
base_dir: str,
|
|
509
|
+
date_format: str,
|
|
510
|
+
verbose: bool,
|
|
511
|
+
dry_run: bool,
|
|
512
|
+
no_exec: bool,
|
|
513
|
+
) -> None:
|
|
504
514
|
"""Create and navigate to today's workspace directory.
|
|
505
515
|
|
|
506
516
|
Creates a date-organized directory (default: ~/Downloads/YYYYMMDD)
|
|
@@ -529,7 +539,7 @@ def today(output_for_cd, base_dir, date_format, verbose, dry_run, no_exec):
|
|
|
529
539
|
|
|
530
540
|
|
|
531
541
|
@cli.command(name="list")
|
|
532
|
-
def list_commands():
|
|
542
|
+
def list_commands() -> int:
|
|
533
543
|
"""List all available fx commands."""
|
|
534
544
|
click.echo("\nAvailable fx commands:\n")
|
|
535
545
|
|
|
@@ -548,7 +558,7 @@ def list_commands():
|
|
|
548
558
|
|
|
549
559
|
@cli.command()
|
|
550
560
|
@click.pass_context
|
|
551
|
-
def help(ctx):
|
|
561
|
+
def help(ctx: click.Context) -> int:
|
|
552
562
|
"""Show help information (same as fx -h).
|
|
553
563
|
|
|
554
564
|
Examples:
|
|
@@ -566,7 +576,7 @@ def help(ctx):
|
|
|
566
576
|
|
|
567
577
|
|
|
568
578
|
@cli.command()
|
|
569
|
-
def version():
|
|
579
|
+
def version() -> int:
|
|
570
580
|
"""Show version and system information.
|
|
571
581
|
|
|
572
582
|
Examples:
|
|
@@ -577,7 +587,7 @@ def version():
|
|
|
577
587
|
return 0
|
|
578
588
|
|
|
579
589
|
|
|
580
|
-
def main():
|
|
590
|
+
def main() -> Any:
|
|
581
591
|
"""Main entry point for the fx command."""
|
|
582
592
|
try:
|
|
583
593
|
return cli()
|
|
@@ -4,16 +4,25 @@ import math
|
|
|
4
4
|
import os
|
|
5
5
|
from dataclasses import dataclass
|
|
6
6
|
from datetime import datetime
|
|
7
|
-
from enum import Enum
|
|
8
7
|
from functools import total_ordering
|
|
9
8
|
from typing import Any, Optional, Set, Tuple
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
10
|
+
# Import shared types to avoid duplication
|
|
11
|
+
from fx_bin.shared_types import EntryType
|
|
12
|
+
|
|
13
|
+
# Re-export for backward compatibility
|
|
14
|
+
__all__ = [
|
|
15
|
+
"EntryType",
|
|
16
|
+
"SizeEntry",
|
|
17
|
+
"FileCountEntry",
|
|
18
|
+
"convert_size",
|
|
19
|
+
"format_size_aligned",
|
|
20
|
+
"generate_timestamp",
|
|
21
|
+
"get_multi_ext",
|
|
22
|
+
"get_base_name",
|
|
23
|
+
"sum_folder_size",
|
|
24
|
+
"sum_folder_files_count",
|
|
25
|
+
]
|
|
17
26
|
|
|
18
27
|
|
|
19
28
|
def generate_timestamp(format_str: str, now: Optional[datetime] = None) -> str:
|