Coverage for enderchest/remote.py: 86%

84 statements  

« prev     ^ index     » next       coverage.py v7.4.1, created at 2024-02-06 16:00 +0000

1"""Higher-level functionality around synchronizing with different EnderCherts""" 

2 

3import logging 

4from pathlib import Path 

5from time import sleep 

6from typing import Sequence 

7from urllib.parse import ParseResult, urlparse 

8 

9from . import filesystem as fs 

10from . import gather, place 

11from .enderchest import EnderChest 

12from .loggers import IMPORTANT, SYNC_LOGGER 

13from .prompt import confirm 

14from .sync import abspath_from_uri, pull, push, remote_file, render_remote 

15 

16 

17def load_remote_ender_chest(uri: str | ParseResult) -> EnderChest: 

18 """Load an EnderChest configuration from another machine 

19 

20 Parameters 

21 ---------- 

22 uri : URI 

23 The URI to the remote Minecraft root 

24 

25 Returns 

26 ------- 

27 EnderChest 

28 The remote EnderChest configuration 

29 

30 Raises 

31 ------ 

32 ValueError 

33 If the provided URI is invalid 

34 RuntimeError 

35 If the config from the remote EnderChest could not be parsed 

36 """ 

37 try: 

38 uri = uri if isinstance(uri, ParseResult) else urlparse(uri) 

39 except AttributeError as bad_uri: 

40 raise ValueError(f"{uri} is not a valid URI") from bad_uri 

41 

42 remote_root = Path(uri.path) 

43 remote_config_path = fs.ender_chest_config(remote_root, check_exists=False) 

44 uri = uri._replace(path=remote_config_path.as_posix()) 

45 

46 with remote_file(uri) as remote_config: 

47 try: 

48 return EnderChest.from_cfg(remote_config) 

49 except ValueError as bad_chest: 

50 raise RuntimeError( 

51 "The remote EnderChest config downloaded" 

52 f"from {uri.geturl()} could not be parsed." 

53 ) from bad_chest 

54 

55 

56def fetch_remotes_from_a_remote_ender_chest( 

57 uri: str | ParseResult, 

58) -> list[tuple[ParseResult, str]]: 

59 """Grab the list of EnderChests registered with the specified remote EnderChest 

60 

61 Parameters 

62 ---------- 

63 uri : URI 

64 The URI to the remote Minecraft root 

65 

66 Returns 

67 ------- 

68 list of (URI, str) tuples 

69 The URIs of the remote EnderChests, paired with their aliases 

70 

71 Raises 

72 ------ 

73 RuntimeError 

74 If the remote list could not be pulled 

75 """ 

76 remote_chest = load_remote_ender_chest(uri) 

77 remotes: list[tuple[ParseResult, str]] = [ 

78 (urlparse(uri) if isinstance(uri, str) else uri, remote_chest.name) 

79 ] 

80 

81 remotes.extend(remote_chest.remotes) 

82 SYNC_LOGGER.info( 

83 "Loaded the following remotes:\n %s", 

84 "\n".join(f" - {render_remote(alias, uri)}" for uri, alias in remotes), 

85 ) 

86 

87 if len(set(alias for _, alias in remotes)) != len(remotes): 

88 raise RuntimeError( 

89 f"There are duplicates aliases in the list of remotes pulled from {uri}" 

90 ) 

91 return remotes 

92 

93 

94def sync_with_remotes( 

95 minecraft_root: Path, 

96 pull_or_push: str, 

97 dry_run: bool = False, 

98 sync_confirm_wait: bool | int | None = None, 

99 **sync_kwargs, 

100) -> None: 

101 """Pull changes from or push changes to remote EnderChests 

102 

103 Parameters 

104 ---------- 

105 minecraft_root : Path 

106 The root directory that your minecraft stuff (or, at least, the one 

107 that's the parent of your EnderChest folder). This will be used to 

108 construct relative paths. 

109 pull_or_push : str 

110 "pull" or "push" 

111 dry_run: bool, optional 

112 Perform a dry run of the sync operation, reporting the operations\ 

113 that will be performed but not actually carrying them out 

114 sync_confirm_wait : bool or int, optional 

115 The default behavior when syncing EnderChests is to first perform a dry 

116 run of every sync operation and then wait 5 seconds before proceeding with the 

117 real sync. The idea is to give the user time to interrupt the sync if 

118 the dry run looks wrong. This can be changed by either raising or lowering 

119 the value of confirm, by disabling the dry-run-first behavior entirely 

120 (`confirm=False`) or by requiring that the user explicitly confirms 

121 the sync (`confirm=True`). This default behavior can also be modified 

122 in the EnderChest config. This parameter will be ignored when performing 

123 a dry run. 

124 sync_kwargs 

125 Any additional arguments that should be passed into the syncing 

126 operation 

127 

128 Notes 

129 ----- 

130 - When pulling changes, this method will try each remote in the order they 

131 are configured and stop once it has successfully pulled from a remote. 

132 

133 This method will attempt to push local changes to *every* remote 

134 """ 

135 if pull_or_push not in ("pull", "push"): 

136 raise ValueError( 

137 'Invalid choice for sync operation. Choices are "pull" and "push"' 

138 ) 

139 try: 

140 if sync_confirm_wait is None: 

141 sync_confirm_wait = gather.load_ender_chest( 

142 minecraft_root 

143 ).sync_confirm_wait 

144 this_chest = gather.load_ender_chest(minecraft_root) 

145 

146 # I know this is redundant, but we want those logs 

147 remotes = gather.load_ender_chest_remotes( 

148 minecraft_root, log_level=logging.DEBUG 

149 ) 

150 except (FileNotFoundError, ValueError) as bad_chest: 

151 SYNC_LOGGER.error( 

152 f"Could not load EnderChest from {minecraft_root}:\n {bad_chest}" 

153 ) 

154 return 

155 if not remotes: 

156 SYNC_LOGGER.error("EnderChest has no remotes. Aborting.") 

157 return # kinda unnecessary 

158 

159 synced_somewhere = False 

160 exclusions: Sequence[str] = sync_kwargs.pop("exclude", None) or () 

161 for remote_uri, alias in remotes: 

162 if dry_run: 

163 runs: tuple[bool, ...] = (True,) 

164 elif sync_confirm_wait is False or sync_confirm_wait <= 0: 

165 runs = (False,) 

166 else: 

167 runs = (True, False) 

168 for do_dry_run in runs: 

169 if dry_run: 

170 prefix = "Simulating an attempt" 

171 else: 

172 prefix = "Attempting" 

173 try: 

174 if pull_or_push == "pull": 

175 SYNC_LOGGER.log( 

176 IMPORTANT, 

177 f"{prefix} to pull changes from %s", 

178 render_remote(alias, remote_uri), 

179 ) 

180 remote_chest = remote_uri._replace( 

181 path=urlparse( 

182 ( 

183 fs.ender_chest_folder( 

184 abspath_from_uri(remote_uri), 

185 check_exists=False, 

186 ) 

187 ).as_uri() 

188 ).path 

189 ) 

190 pull( 

191 remote_chest, 

192 minecraft_root, 

193 exclude=[ 

194 *this_chest.do_not_sync, 

195 *exclusions, 

196 ], 

197 dry_run=do_dry_run, 

198 **sync_kwargs, 

199 ) 

200 else: 

201 SYNC_LOGGER.log( 

202 IMPORTANT, 

203 f"{prefix} to push changes" 

204 f" to {render_remote(alias, remote_uri)}", 

205 ) 

206 local_chest = fs.ender_chest_folder(minecraft_root) 

207 push( 

208 local_chest, 

209 remote_uri, 

210 exclude=[ 

211 *this_chest.do_not_sync, 

212 *exclusions, 

213 ], 

214 dry_run=do_dry_run, 

215 **sync_kwargs, 

216 ) 

217 except ( 

218 FileNotFoundError, 

219 ValueError, 

220 NotImplementedError, 

221 TimeoutError, 

222 RuntimeError, 

223 ) as sync_fail: 

224 SYNC_LOGGER.warning( 

225 f"Could not sync changes with {render_remote(alias, remote_uri)}:" 

226 f"\n {sync_fail}" 

227 ) 

228 break 

229 if do_dry_run == runs[-1]: 

230 continue 

231 if sync_confirm_wait is True: 

232 if not confirm(default=True): 

233 SYNC_LOGGER.error("Aborting") 

234 return 

235 else: 

236 SYNC_LOGGER.debug(f"Waiting for {sync_confirm_wait} seconds") 

237 sleep(sync_confirm_wait) 

238 else: 

239 synced_somewhere = True 

240 if pull_or_push == "pull": 

241 if this_chest.place_after_open and not dry_run: 

242 place.place_ender_chest( 

243 minecraft_root, 

244 keep_broken_links=False, 

245 keep_stale_links=False, 

246 error_handling="abort", 

247 relative=False, 

248 ) 

249 break 

250 if not synced_somewhere: 

251 SYNC_LOGGER.error("Could not sync with any remote EnderChests")