Coverage for enderchest/sync/__init__.py: 97%

37 statements  

« prev     ^ index     » next       coverage.py v7.5.0, created at 2024-05-04 01:41 +0000

1"""Low-level functionality for synchronizing across different machines""" 

2 

3import importlib 

4from contextlib import contextmanager 

5from pathlib import Path 

6from tempfile import TemporaryDirectory 

7from typing import Collection, Generator 

8from urllib.parse import ParseResult 

9 

10from ..loggers import SYNC_LOGGER 

11from .utils import Operation as Op 

12from .utils import ( 

13 abspath_from_uri, 

14 diff, 

15 filter_contents, 

16 generate_sync_report, 

17 get_default_netloc, 

18 is_identical, 

19 render_remote, 

20 uri_to_ssh, 

21) 

22 

23PROTOCOLS = ("rsync", "sftp", "file") 

24 

25 

26def _determine_available_protocols() -> tuple[str, ...]: 

27 """Determine which protocols are available on this system""" 

28 available_protocols: list[str] = [] 

29 for protocol in PROTOCOLS: 

30 try: 

31 _ = importlib.import_module(f"{__package__}.{protocol}") 

32 available_protocols.append(protocol) 

33 except (ModuleNotFoundError, RuntimeError): 

34 pass 

35 return tuple(available_protocols) 

36 

37 

38SUPPORTED_PROTOCOLS = _determine_available_protocols() 

39 

40DEFAULT_PROTOCOL = SUPPORTED_PROTOCOLS[0] 

41 

42 

43def pull( 

44 remote_uri: ParseResult, 

45 local_path: Path, 

46 exclude: Collection[str] | None = None, 

47 dry_run: bool = False, 

48 **kwargs, 

49) -> None: 

50 """Pull all upstream changes from a remote into the specified location 

51 

52 Parameters 

53 ---------- 

54 remote_uri : ParseResult 

55 The URI for the remote resource to pull 

56 local_path : Path 

57 The local destination 

58 exclude : list of str, optional 

59 Any patterns that should be excluded from the sync 

60 dry_run : bool, optional 

61 Whether to only simulate this sync (report the operations to be performed 

62 but not actually perform them). Default is False. 

63 **kwargs 

64 Any additional options to pass into the sync command 

65 """ 

66 try: 

67 protocol = importlib.import_module(f"{__package__}.{remote_uri.scheme.lower()}") 

68 protocol.pull(remote_uri, local_path, exclude or (), dry_run, **kwargs) 

69 except ModuleNotFoundError as not_installed: # pragma: no cover 

70 raise NotImplementedError( 

71 f"Protocol {remote_uri.scheme} is not currently implemented" 

72 ) from not_installed 

73 

74 

75def push( 

76 local_path: Path, 

77 remote_uri: ParseResult, 

78 exclude: Collection[str] | None = None, 

79 dry_run: bool = False, 

80 **kwargs, 

81) -> None: 

82 """Push all local changes in the specified directory into the specified remote 

83 

84 Parameters 

85 ---------- 

86 local_path 

87 The local path to push 

88 remote_uri : ParseResult 

89 The URI for the remote destination 

90 exclude : list of str, optional 

91 Any patterns that should be excluded from the sync 

92 dry_run : bool, optional 

93 Whether to only simulate this sync (report the operations to be performed 

94 but not actually perform them). Default is False. 

95 **kwargs 

96 Any additional options to pass into the sync command 

97 """ 

98 try: 

99 protocol = importlib.import_module(f"{__package__}.{remote_uri.scheme.lower()}") 

100 protocol.push(local_path, remote_uri, exclude or (), dry_run, **kwargs) 

101 except ModuleNotFoundError as not_installed: 

102 raise NotImplementedError( 

103 f"Protocol {remote_uri.scheme} is not currently implemented" 

104 ) from not_installed 

105 

106 

107@contextmanager 

108def remote_file(uri: ParseResult) -> Generator[Path, None, None]: 

109 """Grab a file from a remote filesystem by its URI and read its contents 

110 

111 Parameters 

112 ---------- 

113 uri : parsed URI 

114 The URI of the file to read 

115 

116 Yields 

117 ------ 

118 Path 

119 A path to a local (temp) copy of the file 

120 """ 

121 with TemporaryDirectory(ignore_cleanup_errors=True) as tmpdir: 

122 pull(uri, Path(tmpdir), verbosity=-1) 

123 yield Path(tmpdir) / Path(uri.path).name 

124 

125 

126__all__ = [ 

127 "SYNC_LOGGER", 

128 "PROTOCOLS", 

129 "DEFAULT_PROTOCOL", 

130 "get_default_netloc", 

131 "render_remote", 

132 "remote_file", 

133 "abspath_from_uri", 

134 "pull", 

135 "push", 

136 "uri_to_ssh", 

137]