FileBackup 0.4.3__tar.gz → 0.5.1__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.
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/workflows/CICD_impl.yml +8 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/BuildBinary.py +49 -6
- {filebackup-0.4.3 → filebackup-0.5.1}/PKG-INFO +2 -2
- {filebackup-0.4.3 → filebackup-0.5.1}/pyproject.toml +2 -2
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/CommandLine/OffsiteEntryPoint.py +7 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/Offsite.py +224 -161
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/__init__.py +1 -1
- {filebackup-0.4.3 → filebackup-0.5.1}/tests/CommandLine/EntryPoint_UnitTest.py +3 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/uv.lock +35 -9
- {filebackup-0.4.3 → filebackup-0.5.1}/.copier-answers.yml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/CODEOWNERS +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/ISSUE_TEMPLATE/custom.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/actions/release_impl/action.yml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/pull_request_template.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/release_sources.yaml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/workflows/CICD.yml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.github/workflows/codeql.yml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.gitignore +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.pre-commit-config.yaml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/.python-version +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/CODE_OF_CONDUCT.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/CONTRIBUTING.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/DEVELOPMENT.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/GOVERNANCE.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/LICENSE +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/MAINTAINERS.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/README.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/SECURITY.md +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/minisign_key.pub +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/AutoGitSemVer.yaml +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/CommandLine/CommandLineArguments.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/CommandLine/EntryPoint.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/CommandLine/MirrorEntryPoint.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/CommandLine/__init__.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/FastGlacierDataStore.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/FileSystemDataStore.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/Interfaces/BulkStorageDataStore.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/Interfaces/DataStore.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/Interfaces/FileBasedDataStore.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/Interfaces/__init__.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/S3BrowserDataStore.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/SFTPDataStore.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/__init__.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/Impl/Common.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/Impl/__init__.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/Mirror.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/Snapshot.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/py.typed +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/tests/Mirror_UnitTest.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/tests/Offsite_UnitTest.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/tests/Snapshot_UnitTest.py +0 -0
- {filebackup-0.4.3 → filebackup-0.5.1}/tests/TestHelpers.py +0 -0
|
@@ -66,6 +66,9 @@ jobs:
|
|
|
66
66
|
python-version: ${{ matrix.python_version }}
|
|
67
67
|
enable-cache: true
|
|
68
68
|
|
|
69
|
+
- name: Install Dependencies
|
|
70
|
+
run: uv sync --frozen
|
|
71
|
+
|
|
69
72
|
- name: Run pre-commit scripts
|
|
70
73
|
run: uv run pre-commit run --verbose
|
|
71
74
|
|
|
@@ -250,6 +253,9 @@ jobs:
|
|
|
250
253
|
python-version: ${{ inputs.python_package_version }}
|
|
251
254
|
enable-cache: true
|
|
252
255
|
|
|
256
|
+
- name: Install Dependencies
|
|
257
|
+
run: uv sync --frozen
|
|
258
|
+
|
|
253
259
|
- name: Build Binary
|
|
254
260
|
run: uv run python BuildBinary.py Build --verbose
|
|
255
261
|
|
|
@@ -261,6 +267,8 @@ jobs:
|
|
|
261
267
|
with:
|
|
262
268
|
name: Binary.${{ matrix.os }}
|
|
263
269
|
path: FileBackup-${{ env.PACKAGE_VERSION }}.*
|
|
270
|
+
|
|
271
|
+
# ----------------------------------------------------------------------
|
|
264
272
|
validate_binary:
|
|
265
273
|
needs: [python_package, build_binary]
|
|
266
274
|
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import shutil
|
|
3
|
+
import textwrap
|
|
4
|
+
import uuid
|
|
3
5
|
|
|
4
6
|
from pathlib import Path
|
|
5
7
|
from typing import Annotated, Optional
|
|
6
8
|
|
|
7
9
|
import typer
|
|
8
10
|
|
|
11
|
+
from dbrownell_Common.ContextlibEx import ExitStack
|
|
9
12
|
from dbrownell_Common import SubprocessEx
|
|
10
13
|
from dbrownell_Common.Streams.DoneManager import DoneManager, Flags as DoneManagerFlags
|
|
14
|
+
import inflect
|
|
11
15
|
from typer.core import TyperGroup
|
|
12
16
|
|
|
17
|
+
import FileBackup
|
|
18
|
+
|
|
13
19
|
|
|
14
20
|
# ----------------------------------------------------------------------
|
|
15
21
|
class NaturalOrderGrouper(TyperGroup):
|
|
@@ -46,12 +52,49 @@ def Build(
|
|
|
46
52
|
with DoneManager.CreateCommandLine(
|
|
47
53
|
flags=DoneManagerFlags.Create(verbose=verbose, debug=debug),
|
|
48
54
|
) as dm:
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
+
# inflect uses the typeguard library, which relies on inspect to read the source code of
|
|
56
|
+
# inflect to enforce that arguments are the correct types when invoking functions. This
|
|
57
|
+
# works fine when running the application normally, but does not work by default with
|
|
58
|
+
# cx_Freeze because it does not include the source code of inflect in the built binary. This
|
|
59
|
+
# means that typeguard can't determine the types of the arguments, which causes it to raise
|
|
60
|
+
# an exception. The fix is to include the inflect source code in the built binary so that
|
|
61
|
+
# typeguard can find the function's types and not raise an exception.
|
|
62
|
+
inflect_filename = Path(inflect.__file__).relative_to(Path(__file__).parent)
|
|
63
|
+
|
|
64
|
+
configuration_filename = Path("setup{}.py".format(str(uuid.uuid4()).replace("-", "")))
|
|
65
|
+
|
|
66
|
+
configuration_filename.write_text(
|
|
67
|
+
textwrap.dedent(
|
|
68
|
+
f"""\
|
|
69
|
+
from cx_Freeze import setup, Executable
|
|
70
|
+
|
|
71
|
+
setup(
|
|
72
|
+
name = "FileBackup",
|
|
73
|
+
version = "{FileBackup.__version__}",
|
|
74
|
+
options = {{
|
|
75
|
+
"build_exe": {{
|
|
76
|
+
"include_files": [
|
|
77
|
+
(r"{inflect_filename}", "lib/inflect/{inflect_filename.name}"),
|
|
78
|
+
],
|
|
79
|
+
"packages": []
|
|
80
|
+
}},
|
|
81
|
+
}},
|
|
82
|
+
executables = [
|
|
83
|
+
Executable(
|
|
84
|
+
"src/FileBackup/CommandLine/EntryPoint.py",
|
|
85
|
+
target_name="FileBackup",
|
|
86
|
+
base=None,
|
|
87
|
+
),
|
|
88
|
+
],
|
|
89
|
+
)
|
|
90
|
+
""",
|
|
91
|
+
),
|
|
92
|
+
encoding="utf-8",
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
with ExitStack(configuration_filename.unlink):
|
|
96
|
+
with dm.YieldStream() as stream:
|
|
97
|
+
dm.result = SubprocessEx.Stream(f'python "{configuration_filename}" build_exe', stream)
|
|
55
98
|
|
|
56
99
|
|
|
57
100
|
# ----------------------------------------------------------------------
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: FileBackup
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.1
|
|
4
4
|
Summary: Tool for creating and restoring file system backups.
|
|
5
5
|
Project-URL: Homepage, https://github.com/davidbrownell/FileBackup
|
|
6
6
|
Project-URL: Documentation, https://github.com/davidbrownell/FileBackup
|
|
@@ -28,7 +28,7 @@ Classifier: Topic :: System :: Archiving :: Mirroring
|
|
|
28
28
|
Classifier: Topic :: System :: Systems Administration
|
|
29
29
|
Classifier: Topic :: Utilities
|
|
30
30
|
Requires-Python: >=3.10
|
|
31
|
-
Requires-Dist: dbrownell-common>=0.
|
|
31
|
+
Requires-Dist: dbrownell-common>=0.15.0
|
|
32
32
|
Requires-Dist: paramiko>=3.5.1
|
|
33
33
|
Requires-Dist: typer>=0.15.3
|
|
34
34
|
Description-Content-Type: text/markdown
|
|
@@ -7,7 +7,7 @@ authors = [
|
|
|
7
7
|
]
|
|
8
8
|
requires-python = ">= 3.10"
|
|
9
9
|
dependencies = [
|
|
10
|
-
"dbrownell-common>=0.
|
|
10
|
+
"dbrownell-common>=0.15.0",
|
|
11
11
|
"paramiko>=3.5.1",
|
|
12
12
|
"typer>=0.15.3",
|
|
13
13
|
]
|
|
@@ -23,7 +23,7 @@ keywords = [
|
|
|
23
23
|
license = "MIT"
|
|
24
24
|
|
|
25
25
|
classifiers = [
|
|
26
|
-
"Development Status :: 4 - Beta",
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
27
|
"Environment :: Console",
|
|
28
28
|
"Intended Audience :: Developers",
|
|
29
29
|
"Intended Audience :: End Users/Desktop",
|
|
@@ -241,6 +241,12 @@ def Restore( # pylint: disable=dangerous-default-value
|
|
|
241
241
|
help="Working directory to use when decompressing archives; provide this value during a dry run and subsequent execution to only download and extract the backup content once.",
|
|
242
242
|
),
|
|
243
243
|
] = None,
|
|
244
|
+
continue_on_errors: Annotated[
|
|
245
|
+
bool,
|
|
246
|
+
typer.Option(
|
|
247
|
+
"--continue-on-errors", help="Continue restoring files even if some files cannot be restored."
|
|
248
|
+
),
|
|
249
|
+
] = False,
|
|
244
250
|
) -> None:
|
|
245
251
|
"""Restores content from an offsite data store."""
|
|
246
252
|
|
|
@@ -263,6 +269,7 @@ def Restore( # pylint: disable=dangerous-default-value
|
|
|
263
269
|
quiet=quiet,
|
|
264
270
|
dry_run=dry_run,
|
|
265
271
|
overwrite=overwrite,
|
|
272
|
+
continue_on_errors=continue_on_errors,
|
|
266
273
|
)
|
|
267
274
|
|
|
268
275
|
|
|
@@ -30,16 +30,17 @@ import uuid
|
|
|
30
30
|
from contextlib import contextmanager
|
|
31
31
|
from dataclasses import dataclass
|
|
32
32
|
from enum import auto, Enum
|
|
33
|
+
from io import StringIO
|
|
33
34
|
from pathlib import Path
|
|
34
35
|
from typing import Any, Callable, cast, Iterator, Pattern
|
|
35
36
|
|
|
36
|
-
from dbrownell_Common.ContextlibEx import ExitStack
|
|
37
|
-
from dbrownell_Common import ExecuteTasks
|
|
38
|
-
from dbrownell_Common.InflectEx import inflect
|
|
39
|
-
from dbrownell_Common import PathEx
|
|
40
|
-
from dbrownell_Common.Streams.DoneManager import DoneManager
|
|
41
|
-
from dbrownell_Common import SubprocessEx
|
|
42
|
-
from dbrownell_Common import TextwrapEx
|
|
37
|
+
from dbrownell_Common.ContextlibEx import ExitStack
|
|
38
|
+
from dbrownell_Common import ExecuteTasks
|
|
39
|
+
from dbrownell_Common.InflectEx import inflect
|
|
40
|
+
from dbrownell_Common import PathEx
|
|
41
|
+
from dbrownell_Common.Streams.DoneManager import DoneManager, Flags as DoneManagerFlags
|
|
42
|
+
from dbrownell_Common import SubprocessEx
|
|
43
|
+
from dbrownell_Common import TextwrapEx
|
|
43
44
|
|
|
44
45
|
from FileBackup.DataStore.FileSystemDataStore import FileSystemDataStore
|
|
45
46
|
from FileBackup.DataStore.Interfaces.BulkStorageDataStore import BulkStorageDataStore
|
|
@@ -530,6 +531,7 @@ def Restore(
|
|
|
530
531
|
quiet: bool,
|
|
531
532
|
dry_run: bool,
|
|
532
533
|
overwrite: bool,
|
|
534
|
+
continue_on_errors: bool = False,
|
|
533
535
|
) -> None:
|
|
534
536
|
with Common.YieldDataStore(
|
|
535
537
|
dm,
|
|
@@ -555,7 +557,7 @@ def Restore(
|
|
|
555
557
|
)
|
|
556
558
|
return
|
|
557
559
|
|
|
558
|
-
with _YieldTempDirectory("staging content") as staging_directory:
|
|
560
|
+
with _YieldTempDirectory(working_dir / "staging", "staging content") as staging_directory:
|
|
559
561
|
# ----------------------------------------------------------------------
|
|
560
562
|
@dataclass(frozen=True)
|
|
561
563
|
class Instruction:
|
|
@@ -708,90 +710,118 @@ def Restore(
|
|
|
708
710
|
# ----------------------------------------------------------------------
|
|
709
711
|
def ExecuteTask(
|
|
710
712
|
status: ExecuteTasks.Status,
|
|
711
|
-
) ->
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
713
|
+
) -> ExecuteTasks.TransformResultComplete:
|
|
714
|
+
# This function will create the following directory structure:
|
|
715
|
+
#
|
|
716
|
+
# <working_dir>
|
|
717
|
+
# └── <directory>
|
|
718
|
+
# └── transferred (temporary)
|
|
719
|
+
# └── decompressed (temporary)
|
|
720
|
+
# └── final
|
|
721
|
+
|
|
722
|
+
sink = StringIO()
|
|
723
|
+
|
|
724
|
+
with DoneManager.Create(
|
|
725
|
+
sink,
|
|
726
|
+
"",
|
|
727
|
+
line_prefix="",
|
|
728
|
+
flags=DoneManagerFlags.Create(verbose=dm.is_verbose, debug=dm.is_debug),
|
|
729
|
+
) as dm_sink:
|
|
730
|
+
assert working_dir is not None
|
|
731
|
+
this_working_dir = working_dir / directory
|
|
732
|
+
final_dir = this_working_dir / "final"
|
|
733
|
+
|
|
734
|
+
if final_dir.is_dir():
|
|
735
|
+
# The destination already exists, no need to process it further
|
|
736
|
+
return ExecuteTasks.TransformResultComplete(final_dir, 0)
|
|
737
|
+
|
|
738
|
+
with _YieldTransferredArchive(
|
|
739
|
+
data_store, # type: ignore
|
|
730
740
|
directory,
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
lambda message: cast(
|
|
741
|
+
this_working_dir / "transferred",
|
|
742
|
+
lambda bytes_transferred: cast(
|
|
734
743
|
None,
|
|
735
744
|
status.OnProgress(
|
|
736
|
-
ProcessDirectoryState.
|
|
737
|
-
|
|
745
|
+
ProcessDirectoryState.Transferring.value + 1,
|
|
746
|
+
bytes_transferred,
|
|
738
747
|
),
|
|
739
748
|
),
|
|
740
|
-
) as (
|
|
741
|
-
|
|
742
|
-
|
|
749
|
+
) as (transferred_dir, transferred_dir_is_temporary):
|
|
750
|
+
with _YieldDecompressedFiles(
|
|
751
|
+
dm_sink,
|
|
752
|
+
transferred_dir,
|
|
753
|
+
this_working_dir / "decompressed",
|
|
743
754
|
directory,
|
|
744
|
-
|
|
755
|
+
encryption_password,
|
|
745
756
|
lambda message: cast(
|
|
746
757
|
None,
|
|
747
758
|
status.OnProgress(
|
|
748
|
-
ProcessDirectoryState.
|
|
759
|
+
ProcessDirectoryState.Extracting.value + 1,
|
|
749
760
|
message,
|
|
750
761
|
),
|
|
751
762
|
),
|
|
752
|
-
|
|
763
|
+
continue_on_errors=continue_on_errors,
|
|
764
|
+
) as (decompressed_dir, decompressed_dir_is_temporary):
|
|
765
|
+
# Validate the contents
|
|
766
|
+
_VerifyFiles(
|
|
767
|
+
dm_sink,
|
|
768
|
+
directory,
|
|
769
|
+
decompressed_dir,
|
|
770
|
+
lambda message: cast(
|
|
771
|
+
None,
|
|
772
|
+
status.OnProgress(
|
|
773
|
+
ProcessDirectoryState.Verifying.value + 1,
|
|
774
|
+
message,
|
|
775
|
+
),
|
|
776
|
+
),
|
|
777
|
+
continue_on_errors=continue_on_errors,
|
|
778
|
+
)
|
|
753
779
|
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
780
|
+
# Move/Copy the content. Note that the code assumes a flat
|
|
781
|
+
# directory structure and doesn't do anything to account for
|
|
782
|
+
# nested dirs. This assumption matches the current archive
|
|
783
|
+
# format.
|
|
784
|
+
if transferred_dir_is_temporary or decompressed_dir_is_temporary:
|
|
785
|
+
func = cast(Callable[[Path, Path], None], shutil.move)
|
|
786
|
+
else:
|
|
787
|
+
# ----------------------------------------------------------------------
|
|
788
|
+
def CreateSymLink(
|
|
789
|
+
source: Path,
|
|
790
|
+
dest: Path,
|
|
791
|
+
) -> None:
|
|
792
|
+
dest /= source.name
|
|
793
|
+
os.symlink(source, dest, target_is_directory=source.is_dir())
|
|
768
794
|
|
|
769
|
-
|
|
795
|
+
# ----------------------------------------------------------------------
|
|
770
796
|
|
|
771
|
-
|
|
797
|
+
func = CreateSymLink
|
|
772
798
|
|
|
773
|
-
|
|
799
|
+
temp_dest_dir = final_dir.parent / (final_dir.name + "__temp__")
|
|
774
800
|
|
|
775
|
-
|
|
776
|
-
|
|
801
|
+
shutil.rmtree(temp_dest_dir, ignore_errors=True)
|
|
802
|
+
temp_dest_dir.mkdir(parents=True)
|
|
777
803
|
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
804
|
+
items = [
|
|
805
|
+
item
|
|
806
|
+
for item in decompressed_dir.iterdir()
|
|
807
|
+
if item.name != INDEX_HASH_FILENAME
|
|
808
|
+
]
|
|
783
809
|
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
810
|
+
for item_index, item in enumerate(items):
|
|
811
|
+
status.OnProgress(
|
|
812
|
+
ProcessDirectoryState.Moving.value + 1,
|
|
813
|
+
f"Moving {item_index + 1} of {len(items)}...",
|
|
814
|
+
)
|
|
815
|
+
|
|
816
|
+
if item.exists():
|
|
817
|
+
func(item, temp_dest_dir)
|
|
789
818
|
|
|
790
|
-
|
|
819
|
+
shutil.move(temp_dest_dir, final_dir)
|
|
791
820
|
|
|
792
|
-
|
|
821
|
+
if dm_sink.result != 0:
|
|
822
|
+
status.Log(sink.getvalue())
|
|
793
823
|
|
|
794
|
-
return
|
|
824
|
+
return ExecuteTasks.TransformResultComplete(final_dir, dm_sink.result)
|
|
795
825
|
|
|
796
826
|
# ----------------------------------------------------------------------
|
|
797
827
|
|
|
@@ -799,7 +829,7 @@ def Restore(
|
|
|
799
829
|
|
|
800
830
|
# ----------------------------------------------------------------------
|
|
801
831
|
|
|
802
|
-
directory_working_dirs: list[
|
|
832
|
+
directory_working_dirs: list[object | None | Exception] = ExecuteTasks.TransformTasksEx(
|
|
803
833
|
preprocess_dm,
|
|
804
834
|
"Processing",
|
|
805
835
|
[ExecuteTasks.TaskData(str(directory), directory) for directory in directories],
|
|
@@ -809,7 +839,8 @@ def Restore(
|
|
|
809
839
|
refresh_per_second=Common.EXECUTE_TASKS_REFRESH_PER_SECOND,
|
|
810
840
|
)
|
|
811
841
|
|
|
812
|
-
if
|
|
842
|
+
# Allow the process to continue if warnings were encountered
|
|
843
|
+
if preprocess_dm.result < 0:
|
|
813
844
|
return
|
|
814
845
|
|
|
815
846
|
assert all(isinstance(working_dir, Path) for working_dir in directory_working_dirs), (
|
|
@@ -960,13 +991,16 @@ def Restore(
|
|
|
960
991
|
with dm.Nested("\nProcessing instructions...") as all_instructions_dm:
|
|
961
992
|
all_instructions_dm.WriteLine("")
|
|
962
993
|
|
|
963
|
-
temp_directory =
|
|
994
|
+
temp_directory = working_dir / "instructions"
|
|
995
|
+
temp_directory.mkdir(parents=True, exist_ok=True)
|
|
964
996
|
|
|
965
997
|
with ExitStack(lambda: shutil.rmtree(temp_directory)):
|
|
966
998
|
commit_actions: list[Callable[[], None]] = []
|
|
967
999
|
|
|
968
1000
|
# ----------------------------------------------------------------------
|
|
969
1001
|
def WriteImpl(
|
|
1002
|
+
dm: DoneManager,
|
|
1003
|
+
action: str,
|
|
970
1004
|
local_filename: Path,
|
|
971
1005
|
content_filename: Path | None,
|
|
972
1006
|
) -> None:
|
|
@@ -985,25 +1019,29 @@ def Restore(
|
|
|
985
1019
|
commit_actions.append(CommitDir)
|
|
986
1020
|
return
|
|
987
1021
|
|
|
1022
|
+
content_filename = content_filename.resolve()
|
|
988
1023
|
temp_filename = temp_directory / str(uuid.uuid4())
|
|
989
1024
|
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1025
|
+
if not content_filename.is_file():
|
|
1026
|
+
dm.WriteError(f"The file could not be {action} as its archive data is missing.\n")
|
|
1027
|
+
else:
|
|
1028
|
+
with content_filename.open("rb") as source:
|
|
1029
|
+
with temp_filename.open("wb") as dest:
|
|
1030
|
+
dest.write(source.read())
|
|
993
1031
|
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1032
|
+
# ----------------------------------------------------------------------
|
|
1033
|
+
def CommitFile() -> None:
|
|
1034
|
+
if local_filename.is_dir():
|
|
1035
|
+
shutil.rmtree(local_filename)
|
|
1036
|
+
elif local_filename.is_file():
|
|
1037
|
+
local_filename.unlink()
|
|
1000
1038
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1039
|
+
local_filename.parent.mkdir(parents=True, exist_ok=True)
|
|
1040
|
+
shutil.move(temp_filename, local_filename)
|
|
1003
1041
|
|
|
1004
|
-
|
|
1042
|
+
# ----------------------------------------------------------------------
|
|
1005
1043
|
|
|
1006
|
-
|
|
1044
|
+
commit_actions.append(CommitFile)
|
|
1007
1045
|
|
|
1008
1046
|
# ----------------------------------------------------------------------
|
|
1009
1047
|
def OnAddInstruction(
|
|
@@ -1016,7 +1054,7 @@ def Restore(
|
|
|
1016
1054
|
)
|
|
1017
1055
|
return
|
|
1018
1056
|
|
|
1019
|
-
WriteImpl(instruction.local_filename, instruction.file_content_path)
|
|
1057
|
+
WriteImpl(dm, "restored", instruction.local_filename, instruction.file_content_path)
|
|
1020
1058
|
|
|
1021
1059
|
# ----------------------------------------------------------------------
|
|
1022
1060
|
def OnModifyInstruction(
|
|
@@ -1024,7 +1062,7 @@ def Restore(
|
|
|
1024
1062
|
instruction: Instruction,
|
|
1025
1063
|
) -> None:
|
|
1026
1064
|
assert instruction.file_content_path is not None
|
|
1027
|
-
WriteImpl(instruction.local_filename, instruction.file_content_path)
|
|
1065
|
+
WriteImpl(dm, "modified", instruction.local_filename, instruction.file_content_path)
|
|
1028
1066
|
|
|
1029
1067
|
# ----------------------------------------------------------------------
|
|
1030
1068
|
def OnRemoveInstruction(
|
|
@@ -1101,7 +1139,7 @@ def Restore(
|
|
|
1101
1139
|
) as execute_dm:
|
|
1102
1140
|
on_instruction_func(execute_dm, instruction)
|
|
1103
1141
|
|
|
1104
|
-
if execute_dm.result != 0:
|
|
1142
|
+
if execute_dm.result != 0 and not continue_on_errors:
|
|
1105
1143
|
break
|
|
1106
1144
|
|
|
1107
1145
|
instructions_dm.WriteLine("")
|
|
@@ -1287,9 +1325,9 @@ def _CommitFileBasedDataStore(
|
|
|
1287
1325
|
# ----------------------------------------------------------------------
|
|
1288
1326
|
@contextmanager
|
|
1289
1327
|
def _YieldTempDirectory(
|
|
1328
|
+
temp_directory: Path,
|
|
1290
1329
|
desc: str,
|
|
1291
1330
|
) -> Iterator[Path]:
|
|
1292
|
-
temp_directory = PathEx.CreateTempDirectory()
|
|
1293
1331
|
should_delete = True
|
|
1294
1332
|
|
|
1295
1333
|
try:
|
|
@@ -1299,7 +1337,8 @@ def _YieldTempDirectory(
|
|
|
1299
1337
|
raise
|
|
1300
1338
|
finally:
|
|
1301
1339
|
if should_delete:
|
|
1302
|
-
|
|
1340
|
+
if temp_directory.is_dir():
|
|
1341
|
+
shutil.rmtree(temp_directory)
|
|
1303
1342
|
else:
|
|
1304
1343
|
sys.stderr.write(
|
|
1305
1344
|
f"**** The temporary directory '{temp_directory}' was preserved due to errors while {desc}.\n",
|
|
@@ -1308,9 +1347,10 @@ def _YieldTempDirectory(
|
|
|
1308
1347
|
|
|
1309
1348
|
# ----------------------------------------------------------------------
|
|
1310
1349
|
@contextmanager
|
|
1311
|
-
def
|
|
1350
|
+
def _YieldTransferredArchive(
|
|
1312
1351
|
data_store: FileBasedDataStore,
|
|
1313
1352
|
directory: str,
|
|
1353
|
+
working_dir: Path,
|
|
1314
1354
|
status_func: Callable[[str], None],
|
|
1315
1355
|
) -> Iterator[
|
|
1316
1356
|
tuple[
|
|
@@ -1318,6 +1358,8 @@ def _YieldRestoredArchive(
|
|
|
1318
1358
|
bool, # is temporary directory
|
|
1319
1359
|
],
|
|
1320
1360
|
]:
|
|
1361
|
+
"""Transfer content from the data store to the local filesystem."""
|
|
1362
|
+
|
|
1321
1363
|
if data_store.is_local_filesystem:
|
|
1322
1364
|
working_dir = data_store.GetWorkingDir() / directory
|
|
1323
1365
|
assert working_dir.is_dir(), working_dir
|
|
@@ -1327,7 +1369,7 @@ def _YieldRestoredArchive(
|
|
|
1327
1369
|
|
|
1328
1370
|
status_func("Calculating files to transfer...")
|
|
1329
1371
|
|
|
1330
|
-
with _YieldTempDirectory("transferring archive files") as
|
|
1372
|
+
with _YieldTempDirectory(working_dir, "transferring archive files") as working_dir:
|
|
1331
1373
|
# Map the remote filenames to local filenames
|
|
1332
1374
|
filename_map: dict[Path, Path] = {}
|
|
1333
1375
|
|
|
@@ -1339,7 +1381,7 @@ def _YieldRestoredArchive(
|
|
|
1339
1381
|
relative_root = root.relative_to(data_store_dir)
|
|
1340
1382
|
|
|
1341
1383
|
for filename in filenames:
|
|
1342
|
-
filename_map[root / filename] =
|
|
1384
|
+
filename_map[root / filename] = working_dir / relative_root / filename
|
|
1343
1385
|
|
|
1344
1386
|
if not filename_map:
|
|
1345
1387
|
raise Exception(f"The directory '{directory}' does not contain any files.")
|
|
@@ -1359,24 +1401,30 @@ def _YieldRestoredArchive(
|
|
|
1359
1401
|
),
|
|
1360
1402
|
)
|
|
1361
1403
|
|
|
1362
|
-
yield
|
|
1404
|
+
yield working_dir, True
|
|
1363
1405
|
|
|
1364
1406
|
|
|
1365
1407
|
# ----------------------------------------------------------------------
|
|
1366
1408
|
@contextmanager
|
|
1367
|
-
def
|
|
1409
|
+
def _YieldDecompressedFiles(
|
|
1410
|
+
dm: DoneManager,
|
|
1411
|
+
transferred_dir: Path,
|
|
1412
|
+
decompressed_dir: Path,
|
|
1368
1413
|
directory_name: str,
|
|
1369
|
-
archive_dir: Path,
|
|
1370
1414
|
encryption_password: str | None,
|
|
1371
1415
|
status_func: Callable[[str], None],
|
|
1416
|
+
*,
|
|
1417
|
+
continue_on_errors: bool,
|
|
1372
1418
|
) -> Iterator[
|
|
1373
1419
|
tuple[
|
|
1374
1420
|
Path,
|
|
1375
1421
|
bool, # is temporary directory
|
|
1376
1422
|
],
|
|
1377
1423
|
]:
|
|
1378
|
-
|
|
1379
|
-
|
|
1424
|
+
"""Decompress the archive if necessary"""
|
|
1425
|
+
|
|
1426
|
+
if (transferred_dir / INDEX_FILENAME).is_file():
|
|
1427
|
+
yield transferred_dir, False
|
|
1380
1428
|
return
|
|
1381
1429
|
|
|
1382
1430
|
# By default, 7zip will prompt for a password with archives that were created
|
|
@@ -1391,18 +1439,17 @@ def _YieldRestoredFiles(
|
|
|
1391
1439
|
#
|
|
1392
1440
|
password = encryption_password or str(uuid.uuid4())
|
|
1393
1441
|
|
|
1394
|
-
|
|
1395
|
-
status_func("Validating archive...")
|
|
1396
|
-
|
|
1397
|
-
archive_filename = archive_dir / (ARCHIVE_FILENAME + ".001")
|
|
1442
|
+
archive_filename = transferred_dir / (ARCHIVE_FILENAME + ".001")
|
|
1398
1443
|
|
|
1399
1444
|
if not archive_filename.is_file():
|
|
1400
1445
|
raise Exception(f"The archive file '{archive_filename.name}' was not found.")
|
|
1401
1446
|
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1447
|
+
# Validate
|
|
1448
|
+
status_func("Validating archive...")
|
|
1449
|
+
with dm.Nested("Validating archive...") as validate_dm:
|
|
1450
|
+
result = SubprocessEx.Run(f'{_GetZipBinary()} t "{archive_filename}" "-p{password}"')
|
|
1451
|
+
if result.returncode != 0:
|
|
1452
|
+
message = textwrap.dedent(
|
|
1406
1453
|
"""\
|
|
1407
1454
|
Archive validation failed for the directory '{}' ({}).
|
|
1408
1455
|
|
|
@@ -1418,27 +1465,32 @@ def _YieldRestoredFiles(
|
|
|
1418
1465
|
4,
|
|
1419
1466
|
skip_first_line=True,
|
|
1420
1467
|
),
|
|
1421
|
-
)
|
|
1422
|
-
)
|
|
1468
|
+
)
|
|
1423
1469
|
|
|
1424
|
-
|
|
1425
|
-
|
|
1470
|
+
if continue_on_errors:
|
|
1471
|
+
validate_dm.WriteWarning(message)
|
|
1472
|
+
else:
|
|
1473
|
+
raise Exception(message)
|
|
1426
1474
|
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1475
|
+
# Extract
|
|
1476
|
+
with _YieldTempDirectory(decompressed_dir, "extracting the archive") as decompressed_dir:
|
|
1477
|
+
status_func("Extracting archive...")
|
|
1478
|
+
with dm.Nested("Extracting archive...") as extract_dm:
|
|
1479
|
+
decompressed_dir.mkdir(parents=True, exist_ok=True)
|
|
1480
|
+
|
|
1481
|
+
result = SubprocessEx.Run(
|
|
1482
|
+
f'{_GetZipBinary()} x "{archive_filename}" "-p{password}"',
|
|
1483
|
+
cwd=decompressed_dir,
|
|
1484
|
+
)
|
|
1432
1485
|
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
textwrap.dedent(
|
|
1486
|
+
if result.returncode != 0:
|
|
1487
|
+
message = textwrap.dedent(
|
|
1436
1488
|
"""\
|
|
1437
|
-
|
|
1489
|
+
Archive extraction failed for the directory '{}' ({}).
|
|
1438
1490
|
|
|
1439
|
-
|
|
1491
|
+
{}
|
|
1440
1492
|
|
|
1441
|
-
|
|
1493
|
+
""",
|
|
1442
1494
|
).format(
|
|
1443
1495
|
directory_name,
|
|
1444
1496
|
result.returncode,
|
|
@@ -1447,67 +1499,74 @@ def _YieldRestoredFiles(
|
|
|
1447
1499
|
4,
|
|
1448
1500
|
skip_first_line=True,
|
|
1449
1501
|
),
|
|
1450
|
-
)
|
|
1451
|
-
)
|
|
1502
|
+
)
|
|
1452
1503
|
|
|
1453
|
-
|
|
1504
|
+
if continue_on_errors:
|
|
1505
|
+
extract_dm.WriteWarning(message)
|
|
1506
|
+
else:
|
|
1507
|
+
raise Exception(message)
|
|
1508
|
+
|
|
1509
|
+
yield decompressed_dir, True
|
|
1454
1510
|
|
|
1455
1511
|
|
|
1456
1512
|
# ----------------------------------------------------------------------
|
|
1457
|
-
def
|
|
1513
|
+
def _VerifyFiles(
|
|
1514
|
+
dm: DoneManager,
|
|
1458
1515
|
directory_name: str,
|
|
1459
1516
|
contents_dir: Path,
|
|
1460
1517
|
status_func: Callable[[str], None],
|
|
1518
|
+
*,
|
|
1519
|
+
continue_on_errors: bool,
|
|
1461
1520
|
) -> None:
|
|
1462
1521
|
# Ensure that the index is present
|
|
1463
1522
|
for index_filename in [INDEX_FILENAME, INDEX_HASH_FILENAME]:
|
|
1464
1523
|
if not (contents_dir / index_filename).is_file():
|
|
1465
1524
|
raise Exception(f"The index file '{index_filename}' does not exist.")
|
|
1466
1525
|
|
|
1467
|
-
|
|
1468
|
-
|
|
1526
|
+
with dm.Nested("Verifying files...") as verify_dm:
|
|
1527
|
+
# Ensure that the content is valid
|
|
1528
|
+
all_filenames: list[Path] = []
|
|
1469
1529
|
|
|
1470
|
-
|
|
1471
|
-
|
|
1530
|
+
for root_str, _, filenames in os.walk(contents_dir):
|
|
1531
|
+
root = Path(root_str)
|
|
1472
1532
|
|
|
1473
|
-
|
|
1533
|
+
all_filenames += [root / filename for filename in filenames if filename != INDEX_HASH_FILENAME]
|
|
1474
1534
|
|
|
1475
|
-
|
|
1535
|
+
data_store = FileSystemDataStore()
|
|
1476
1536
|
|
|
1477
|
-
|
|
1537
|
+
errors: list[str] = []
|
|
1478
1538
|
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1539
|
+
for filename_index, filename in enumerate(all_filenames):
|
|
1540
|
+
if filename.name == INDEX_FILENAME:
|
|
1541
|
+
expected_hash_value = (contents_dir / INDEX_HASH_FILENAME).read_text().strip()
|
|
1542
|
+
else:
|
|
1543
|
+
expected_hash_value = filename.name
|
|
1484
1544
|
|
|
1485
|
-
|
|
1545
|
+
file_size = filename.stat().st_size or 1
|
|
1486
1546
|
|
|
1487
|
-
|
|
1547
|
+
status_template = f"Validating file {filename_index + 1} of {len(all_filenames)} [{PathEx.GetSizeDisplay(file_size)}] {{:.02f}}%..."
|
|
1488
1548
|
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
),
|
|
1495
|
-
)
|
|
1496
|
-
|
|
1497
|
-
if actual_hash_value != expected_hash_value:
|
|
1498
|
-
errors.append(
|
|
1499
|
-
textwrap.dedent(
|
|
1500
|
-
f"""\
|
|
1501
|
-
Filename: {filename.relative_to(contents_dir)}
|
|
1502
|
-
Expected: {expected_hash_value}
|
|
1503
|
-
Actual: {actual_hash_value}
|
|
1504
|
-
""",
|
|
1549
|
+
actual_hash_value = Common.CalculateHash(
|
|
1550
|
+
data_store,
|
|
1551
|
+
filename,
|
|
1552
|
+
lambda bytes_transferred: status_func(
|
|
1553
|
+
status_template.format((bytes_transferred / file_size) * 100)
|
|
1505
1554
|
),
|
|
1506
1555
|
)
|
|
1507
1556
|
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1557
|
+
if actual_hash_value != expected_hash_value:
|
|
1558
|
+
errors.append(
|
|
1559
|
+
textwrap.dedent(
|
|
1560
|
+
f"""\
|
|
1561
|
+
Filename: {filename.relative_to(contents_dir)}
|
|
1562
|
+
Expected: {expected_hash_value}
|
|
1563
|
+
Actual: {actual_hash_value}
|
|
1564
|
+
""",
|
|
1565
|
+
),
|
|
1566
|
+
)
|
|
1567
|
+
|
|
1568
|
+
if errors:
|
|
1569
|
+
message = textwrap.dedent(
|
|
1511
1570
|
"""\
|
|
1512
1571
|
Corrupt files were encountered in the directory '{}'.
|
|
1513
1572
|
|
|
@@ -1521,5 +1580,9 @@ def _VerifyRestoredFiles(
|
|
|
1521
1580
|
4,
|
|
1522
1581
|
skip_first_line=True,
|
|
1523
1582
|
),
|
|
1524
|
-
)
|
|
1525
|
-
|
|
1583
|
+
)
|
|
1584
|
+
|
|
1585
|
+
if continue_on_errors:
|
|
1586
|
+
verify_dm.WriteWarning(message)
|
|
1587
|
+
else:
|
|
1588
|
+
raise Exception(message)
|
|
@@ -9,4 +9,4 @@
|
|
|
9
9
|
# Note that this value will be overwritten by calls to `python ../../Build.py update_version` based
|
|
10
10
|
# on changes observed in the git repository. The default value below will be used until the value
|
|
11
11
|
# here is explicitly updated by the Continuous Integration system as part of a commit.
|
|
12
|
-
__version__ = "0.
|
|
12
|
+
__version__ = "0.5.1"
|
|
@@ -447,6 +447,7 @@ class TestOffsite:
|
|
|
447
447
|
kwargs = restore.call_args_list[0].kwargs
|
|
448
448
|
|
|
449
449
|
assert kwargs == {
|
|
450
|
+
"continue_on_errors": False,
|
|
450
451
|
"ssd": False,
|
|
451
452
|
"quiet": False,
|
|
452
453
|
"dry_run": False,
|
|
@@ -483,6 +484,7 @@ class TestOffsite:
|
|
|
483
484
|
"--overwrite",
|
|
484
485
|
"--ssd",
|
|
485
486
|
"--quiet",
|
|
487
|
+
"--continue-on-errors",
|
|
486
488
|
]
|
|
487
489
|
|
|
488
490
|
result = CliRunner().invoke(app, args)
|
|
@@ -501,6 +503,7 @@ class TestOffsite:
|
|
|
501
503
|
kwargs = restore.call_args_list[0].kwargs
|
|
502
504
|
|
|
503
505
|
assert kwargs == {
|
|
506
|
+
"continue_on_errors": True,
|
|
504
507
|
"ssd": True,
|
|
505
508
|
"quiet": True,
|
|
506
509
|
"dry_run": True,
|
|
@@ -387,7 +387,7 @@ wheels = [
|
|
|
387
387
|
|
|
388
388
|
[[package]]
|
|
389
389
|
name = "dbrownell-common"
|
|
390
|
-
version = "0.
|
|
390
|
+
version = "0.15.0"
|
|
391
391
|
source = { registry = "https://pypi.org/simple" }
|
|
392
392
|
dependencies = [
|
|
393
393
|
{ name = "inflect" },
|
|
@@ -395,8 +395,9 @@ dependencies = [
|
|
|
395
395
|
{ name = "typer" },
|
|
396
396
|
{ name = "typer-config" },
|
|
397
397
|
]
|
|
398
|
+
sdist = { url = "https://files.pythonhosted.org/packages/75/83/785a3d7e8622f1cb805625f9cca9024c1d6985afc6626efa0946b00d2f27/dbrownell_common-0.15.0.tar.gz", hash = "sha256:6a7e2caa89e2eadef473901f35f5eab2963deb74b335c9ed8f3e05c2ba9f184f", size = 97508, upload-time = "2025-07-03T17:27:55.32Z" }
|
|
398
399
|
wheels = [
|
|
399
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
400
|
+
{ url = "https://files.pythonhosted.org/packages/37/dc/b1663f95bf3582d0fd07498700f4708e71ef9c5e3bc7441d90353884908b/dbrownell_common-0.15.0-py3-none-any.whl", hash = "sha256:554238d000dfc8acc78fa6d9df778706bb4f84f5b70f35f75cf0ccdd8f0f09de", size = 43206, upload-time = "2025-07-03T17:27:54.115Z" },
|
|
400
401
|
]
|
|
401
402
|
|
|
402
403
|
[[package]]
|
|
@@ -464,7 +465,7 @@ dev = [
|
|
|
464
465
|
|
|
465
466
|
[package.metadata]
|
|
466
467
|
requires-dist = [
|
|
467
|
-
{ name = "dbrownell-common", specifier = ">=0.
|
|
468
|
+
{ name = "dbrownell-common", specifier = ">=0.15.0" },
|
|
468
469
|
{ name = "paramiko", specifier = ">=3.5.1" },
|
|
469
470
|
{ name = "typer", specifier = ">=0.15.3" },
|
|
470
471
|
]
|
|
@@ -524,11 +525,15 @@ wheels = [
|
|
|
524
525
|
|
|
525
526
|
[[package]]
|
|
526
527
|
name = "inflect"
|
|
527
|
-
version = "
|
|
528
|
+
version = "7.5.0"
|
|
528
529
|
source = { registry = "https://pypi.org/simple" }
|
|
529
|
-
|
|
530
|
+
dependencies = [
|
|
531
|
+
{ name = "more-itertools" },
|
|
532
|
+
{ name = "typeguard" },
|
|
533
|
+
]
|
|
534
|
+
sdist = { url = "https://files.pythonhosted.org/packages/78/c6/943357d44a21fd995723d07ccaddd78023eace03c1846049a2645d4324a3/inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f", size = 73751, upload-time = "2024-12-28T17:11:18.897Z" }
|
|
530
535
|
wheels = [
|
|
531
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
536
|
+
{ url = "https://files.pythonhosted.org/packages/8a/eb/427ed2b20a38a4ee29f24dbe4ae2dafab198674fe9a85e3d6adf9e5f5f41/inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344", size = 35197, upload-time = "2024-12-28T17:11:15.931Z" },
|
|
532
537
|
]
|
|
533
538
|
|
|
534
539
|
[[package]]
|
|
@@ -615,6 +620,15 @@ wheels = [
|
|
|
615
620
|
{ url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" },
|
|
616
621
|
]
|
|
617
622
|
|
|
623
|
+
[[package]]
|
|
624
|
+
name = "more-itertools"
|
|
625
|
+
version = "10.7.0"
|
|
626
|
+
source = { registry = "https://pypi.org/simple" }
|
|
627
|
+
sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" }
|
|
628
|
+
wheels = [
|
|
629
|
+
{ url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" },
|
|
630
|
+
]
|
|
631
|
+
|
|
618
632
|
[[package]]
|
|
619
633
|
name = "nodeenv"
|
|
620
634
|
version = "1.9.1"
|
|
@@ -823,16 +837,16 @@ wheels = [
|
|
|
823
837
|
|
|
824
838
|
[[package]]
|
|
825
839
|
name = "rich"
|
|
826
|
-
version = "
|
|
840
|
+
version = "14.0.0"
|
|
827
841
|
source = { registry = "https://pypi.org/simple" }
|
|
828
842
|
dependencies = [
|
|
829
843
|
{ name = "markdown-it-py" },
|
|
830
844
|
{ name = "pygments" },
|
|
831
845
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
|
832
846
|
]
|
|
833
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
847
|
+
sdist = { url = "https://files.pythonhosted.org/packages/a1/53/830aa4c3066a8ab0ae9a9955976fb770fe9c6102117c8ec4ab3ea62d89e8/rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725", size = 224078, upload-time = "2025-03-30T14:15:14.23Z" }
|
|
834
848
|
wheels = [
|
|
835
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
849
|
+
{ url = "https://files.pythonhosted.org/packages/0d/9b/63f4c7ebc259242c89b3acafdb37b41d1185c07ff0011164674e9076b491/rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0", size = 243229, upload-time = "2025-03-30T14:15:12.283Z" },
|
|
836
850
|
]
|
|
837
851
|
|
|
838
852
|
[[package]]
|
|
@@ -1052,6 +1066,18 @@ wheels = [
|
|
|
1052
1066
|
{ url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" },
|
|
1053
1067
|
]
|
|
1054
1068
|
|
|
1069
|
+
[[package]]
|
|
1070
|
+
name = "typeguard"
|
|
1071
|
+
version = "4.4.2"
|
|
1072
|
+
source = { registry = "https://pypi.org/simple" }
|
|
1073
|
+
dependencies = [
|
|
1074
|
+
{ name = "typing-extensions" },
|
|
1075
|
+
]
|
|
1076
|
+
sdist = { url = "https://files.pythonhosted.org/packages/70/60/8cd6a3d78d00ceeb2193c02b7ed08f063d5341ccdfb24df88e61f383048e/typeguard-4.4.2.tar.gz", hash = "sha256:a6f1065813e32ef365bc3b3f503af8a96f9dd4e0033a02c28c4a4983de8c6c49", size = 75746, upload-time = "2025-02-16T16:28:26.205Z" }
|
|
1077
|
+
wheels = [
|
|
1078
|
+
{ url = "https://files.pythonhosted.org/packages/cf/4b/9a77dc721aa0b7f74440a42e4ef6f9a4fae7324e17f64f88b96f4c25cc05/typeguard-4.4.2-py3-none-any.whl", hash = "sha256:77a78f11f09777aeae7fa08585f33b5f4ef0e7335af40005b0c422ed398ff48c", size = 35801, upload-time = "2025-02-16T16:28:24.793Z" },
|
|
1079
|
+
]
|
|
1080
|
+
|
|
1055
1081
|
[[package]]
|
|
1056
1082
|
name = "typer"
|
|
1057
1083
|
version = "0.15.3"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/Interfaces/BulkStorageDataStore.py
RENAMED
|
File without changes
|
|
File without changes
|
{filebackup-0.4.3 → filebackup-0.5.1}/src/FileBackup/DataStore/Interfaces/FileBasedDataStore.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|