PyHardLinkBackup 1.5.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.
Files changed (42) hide show
  1. PyHardLinkBackup/__init__.py +7 -0
  2. PyHardLinkBackup/__main__.py +10 -0
  3. PyHardLinkBackup/backup.py +261 -0
  4. PyHardLinkBackup/cli_app/__init__.py +41 -0
  5. PyHardLinkBackup/cli_app/phlb.py +123 -0
  6. PyHardLinkBackup/cli_dev/__init__.py +70 -0
  7. PyHardLinkBackup/cli_dev/benchmark.py +138 -0
  8. PyHardLinkBackup/cli_dev/code_style.py +12 -0
  9. PyHardLinkBackup/cli_dev/packaging.py +65 -0
  10. PyHardLinkBackup/cli_dev/shell_completion.py +23 -0
  11. PyHardLinkBackup/cli_dev/testing.py +52 -0
  12. PyHardLinkBackup/cli_dev/update_readme_history.py +33 -0
  13. PyHardLinkBackup/compare_backup.py +212 -0
  14. PyHardLinkBackup/constants.py +16 -0
  15. PyHardLinkBackup/logging_setup.py +124 -0
  16. PyHardLinkBackup/rebuild_databases.py +176 -0
  17. PyHardLinkBackup/tests/__init__.py +36 -0
  18. PyHardLinkBackup/tests/test_backup.py +628 -0
  19. PyHardLinkBackup/tests/test_compare_backup.py +86 -0
  20. PyHardLinkBackup/tests/test_doc_write.py +26 -0
  21. PyHardLinkBackup/tests/test_doctests.py +10 -0
  22. PyHardLinkBackup/tests/test_project_setup.py +46 -0
  23. PyHardLinkBackup/tests/test_readme.py +75 -0
  24. PyHardLinkBackup/tests/test_readme_history.py +9 -0
  25. PyHardLinkBackup/tests/test_rebuild_database.py +224 -0
  26. PyHardLinkBackup/utilities/__init__.py +0 -0
  27. PyHardLinkBackup/utilities/file_hash_database.py +62 -0
  28. PyHardLinkBackup/utilities/file_size_database.py +46 -0
  29. PyHardLinkBackup/utilities/filesystem.py +158 -0
  30. PyHardLinkBackup/utilities/humanize.py +39 -0
  31. PyHardLinkBackup/utilities/rich_utils.py +99 -0
  32. PyHardLinkBackup/utilities/sha256sums.py +61 -0
  33. PyHardLinkBackup/utilities/tee.py +40 -0
  34. PyHardLinkBackup/utilities/tests/__init__.py +0 -0
  35. PyHardLinkBackup/utilities/tests/test_file_hash_database.py +143 -0
  36. PyHardLinkBackup/utilities/tests/test_file_size_database.py +138 -0
  37. PyHardLinkBackup/utilities/tests/test_filesystem.py +126 -0
  38. PyHardLinkBackup/utilities/tyro_cli_shared_args.py +12 -0
  39. pyhardlinkbackup-1.5.0.dist-info/METADATA +600 -0
  40. pyhardlinkbackup-1.5.0.dist-info/RECORD +42 -0
  41. pyhardlinkbackup-1.5.0.dist-info/WHEEL +4 -0
  42. pyhardlinkbackup-1.5.0.dist-info/entry_points.txt +3 -0
@@ -0,0 +1,126 @@
1
+ import hashlib
2
+ import logging
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+ from unittest.mock import patch
7
+
8
+ from cli_base.cli_tools.test_utils.base_testcases import BaseTestCase
9
+
10
+ from PyHardLinkBackup.constants import HASH_ALGO
11
+ from PyHardLinkBackup.utilities.filesystem import (
12
+ copy_and_hash,
13
+ hash_file,
14
+ iter_scandir_files,
15
+ read_and_hash_file,
16
+ supports_hardlinks,
17
+ )
18
+
19
+
20
+ class TestHashFile(BaseTestCase):
21
+ maxDiff = None
22
+
23
+ def test_hash_file(self):
24
+ self.assertEqual(
25
+ hashlib.new(HASH_ALGO, b'test content').hexdigest(),
26
+ '6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72',
27
+ )
28
+ with tempfile.NamedTemporaryFile() as temp:
29
+ temp_file_path = Path(temp.name)
30
+ temp_file_path.write_bytes(b'test content')
31
+
32
+ with self.assertLogs(level='INFO') as logs:
33
+ file_hash = hash_file(temp_file_path)
34
+ self.assertEqual(file_hash, '6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72')
35
+ self.assertIn(' sha256 hash: 6ae8a7', ''.join(logs.output))
36
+
37
+ def test_copy_and_hash(self):
38
+ with tempfile.TemporaryDirectory() as temp:
39
+ temp_path = Path(temp)
40
+
41
+ src_path = temp_path / 'source.txt'
42
+ dst_path = temp_path / 'dest.txt'
43
+
44
+ src_path.write_bytes(b'test content')
45
+
46
+ with self.assertLogs(level='INFO') as logs:
47
+ file_hash = copy_and_hash(src=src_path, dst=dst_path)
48
+
49
+ self.assertEqual(dst_path.read_bytes(), b'test content')
50
+ self.assertEqual(file_hash, '6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72')
51
+ self.assertIn(' backup to ', ''.join(logs.output))
52
+
53
+ def test_read_and_hash_file(self):
54
+ with tempfile.NamedTemporaryFile() as temp:
55
+ temp_file_path = Path(temp.name)
56
+ temp_file_path.write_bytes(b'test content')
57
+
58
+ with self.assertLogs(level='INFO') as logs:
59
+ content, file_hash = read_and_hash_file(temp_file_path)
60
+ self.assertEqual(content, b'test content')
61
+ self.assertEqual(file_hash, '6ae8a75555209fd6c44157c0aed8016e763ff435a19cf186f76863140143ff72')
62
+ self.assertIn(' sha256 hash: 6ae8a7', ''.join(logs.output))
63
+
64
+ def test_iter_scandir_files(self):
65
+ with tempfile.TemporaryDirectory() as temp:
66
+ temp_path = Path(temp)
67
+
68
+ (temp_path / 'file1.txt').write_bytes(b'content1')
69
+ (temp_path / 'file2.txt').write_bytes(b'content2')
70
+ subdir = temp_path / 'subdir'
71
+ subdir.mkdir()
72
+ (subdir / 'file3.txt').write_bytes(b'content3')
73
+
74
+ # Add a symlink to file1.txt
75
+ (temp_path / 'symlink_to_file1.txt').symlink_to(temp_path / 'file1.txt')
76
+
77
+ # Add a hardlink to file2.txt
78
+ os.link(temp_path / 'file2.txt', temp_path / 'hardlink_to_file2.txt')
79
+
80
+ exclude_subdir = temp_path / '__pycache__'
81
+ exclude_subdir.mkdir()
82
+ (exclude_subdir / 'BAM.txt').write_bytes(b'foobar')
83
+
84
+ broken_symlink_path = temp_path / 'broken_symlink'
85
+ broken_symlink_path.symlink_to(temp_path / 'not/existing/file.txt')
86
+
87
+ with self.assertLogs(level='DEBUG') as logs:
88
+ files = list(iter_scandir_files(temp_path, excludes={'__pycache__'}))
89
+
90
+ file_names = sorted([Path(f.path).relative_to(temp_path).as_posix() for f in files])
91
+
92
+ self.assertEqual(
93
+ file_names,
94
+ [
95
+ 'broken_symlink',
96
+ 'file1.txt',
97
+ 'file2.txt',
98
+ 'hardlink_to_file2.txt',
99
+ 'subdir/file3.txt',
100
+ 'symlink_to_file1.txt',
101
+ ],
102
+ )
103
+ logs = ''.join(logs.output)
104
+ self.assertIn('Scanning directory ', logs)
105
+ self.assertIn('Excluding directory ', logs)
106
+
107
+ def test_supports_hardlinks(self):
108
+ with tempfile.TemporaryDirectory() as temp:
109
+ with self.assertLogs(level=logging.INFO) as logs:
110
+ self.assertTrue(supports_hardlinks(Path(temp)))
111
+ self.assertEqual(
112
+ ''.join(logs.output),
113
+ f'INFO:PyHardLinkBackup.utilities.filesystem:Hardlink support in {temp}: True',
114
+ )
115
+
116
+ with (
117
+ self.assertLogs(level=logging.ERROR) as logs,
118
+ patch('PyHardLinkBackup.utilities.filesystem.os.link', side_effect=OSError),
119
+ ):
120
+ self.assertFalse(supports_hardlinks(Path(temp)))
121
+ logs = ''.join(logs.output)
122
+ self.assertIn(f'Hardlink test failed in {temp}:', logs)
123
+ self.assertIn('OSError', logs)
124
+
125
+ with self.assertLogs(level=logging.DEBUG), self.assertRaises(NotADirectoryError):
126
+ supports_hardlinks(Path('/not/existing/directory'))
@@ -0,0 +1,12 @@
1
+ from typing import Annotated
2
+
3
+ import tyro
4
+
5
+
6
+ TyroExcludeDirectoriesArgType = Annotated[
7
+ tuple[str, ...],
8
+ tyro.conf.arg(
9
+ help='List of directories to exclude from backup.',
10
+ ),
11
+ ]
12
+ DEFAULT_EXCLUDE_DIRECTORIES = ('__pycache__', '.cache', '.temp', '.tmp', '.tox', '.nox')