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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: fx-bin
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: A common bin collection for my own usage
5
5
  Keywords: fx_bin
6
6
  Author: Frank Xu
@@ -15,4 +15,4 @@ try:
15
15
  __version__ = version("fx-bin")
16
16
  except Exception:
17
17
  # Fallback version in case package is not installed
18
- __version__ = "2.2.2"
18
+ __version__ = "2.3.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(keyword, first, include_ignored, excludes):
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(output_for_cd, base_dir, date_format, verbose, dry_run, no_exec):
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
- class EntryType(Enum):
13
- """Type of filesystem entry."""
14
-
15
- FILE = 1
16
- FOLDER = 2
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: