Coverage for enderchest/remote.py: 80%

82 statements  

« prev     ^ index     » next       coverage.py v7.4.0, created at 2024-01-11 17:09 +0000

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

2import logging 

3from pathlib import Path 

4from time import sleep 

5from urllib.parse import ParseResult, urlparse 

6 

7from . import filesystem as fs 

8from . import gather, place 

9from .enderchest import EnderChest 

10from .loggers import IMPORTANT, SYNC_LOGGER 

11from .prompt import confirm 

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

13 

14 

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

16 """Load an EnderChest configuration from another machine 

17 

18 Parameters 

19 ---------- 

20 uri : URI 

21 The URI to the remote Minecraft root 

22 

23 Returns 

24 ------- 

25 EnderChest 

26 The remote EnderChest configuration 

27 

28 Raises 

29 ------ 

30 ValueError 

31 If the provided URI is invalid 

32 RuntimeError 

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

34 """ 

35 try: 

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

37 except AttributeError as bad_uri: 

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

39 

40 remote_root = Path(uri.path) 

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

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

43 

44 with remote_file(uri) as remote_config: 

45 try: 

46 return EnderChest.from_cfg(remote_config) 

47 except ValueError as bad_chest: 

48 raise RuntimeError( 

49 "The remote EnderChest config downloaded" 

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

51 ) from bad_chest 

52 

53 

54def fetch_remotes_from_a_remote_ender_chest( 

55 uri: str | ParseResult, 

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

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

58 

59 Parameters 

60 ---------- 

61 uri : URI 

62 The URI to the remote Minecraft root 

63 

64 Returns 

65 ------- 

66 list of (URI, str) tuples 

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

68 

69 Raises 

70 ------ 

71 RuntimeError 

72 If the remote list could not be pulled 

73 """ 

74 remote_chest = load_remote_ender_chest(uri) 

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

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

77 ] 

78 

79 remotes.extend(remote_chest.remotes) 

80 SYNC_LOGGER.info( 

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

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

83 ) 

84 

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

86 raise RuntimeError( 

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

88 ) 

89 return remotes 

90 

91 

92def sync_with_remotes( 

93 minecraft_root: Path, 

94 pull_or_push: str, 

95 dry_run: bool = False, 

96 sync_confirm_wait: bool | int | None = None, 

97 **sync_kwargs, 

98) -> None: 

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

100 

101 Parameters 

102 ---------- 

103 minecraft_root : Path 

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

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

106 construct relative paths. 

107 pull_or_push : str 

108 "pull" or "push" 

109 dry_run: bool, optional 

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

111 that will be performed but not actually carrying them out 

112 sync_confirm_wait : bool or int, optional 

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

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

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

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

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

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

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

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

121 a dry run. 

122 sync_kwargs 

123 Any additional arguments that should be passed into the syncing 

124 operation 

125 

126 Notes 

127 ----- 

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

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

130 

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

132 """ 

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

134 raise ValueError( 

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

136 ) 

137 try: 

138 if sync_confirm_wait is None: 

139 sync_confirm_wait = gather.load_ender_chest( 

140 minecraft_root 

141 ).sync_confirm_wait 

142 this_chest = gather.load_ender_chest(minecraft_root) 

143 

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

145 remotes = gather.load_ender_chest_remotes( 

146 minecraft_root, log_level=logging.DEBUG 

147 ) 

148 except (FileNotFoundError, ValueError) as bad_chest: 

149 SYNC_LOGGER.error( 

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

151 ) 

152 return 

153 if not remotes: 

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

155 return # kinda unnecessary 

156 

157 synced_somewhere = False 

158 for remote_uri, alias in remotes: 

159 if dry_run: 

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

161 elif sync_confirm_wait is False or sync_confirm_wait <= 0: 

162 runs = (False,) 

163 else: 

164 runs = (True, False) 

165 for do_dry_run in runs: 

166 if dry_run: 

167 prefix = "Simulating an attempt" 

168 else: 

169 prefix = "Attempting" 

170 try: 

171 if pull_or_push == "pull": 

172 SYNC_LOGGER.log( 

173 IMPORTANT, 

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

175 render_remote(alias, remote_uri), 

176 ) 

177 remote_chest = remote_uri._replace( 

178 path=urlparse( 

179 ( 

180 fs.ender_chest_folder( 

181 abspath_from_uri(remote_uri), 

182 check_exists=False, 

183 ) 

184 ).as_uri() 

185 ).path 

186 ) 

187 pull( 

188 remote_chest, 

189 minecraft_root, 

190 exclude=[ 

191 *this_chest.do_not_sync, 

192 *(sync_kwargs.pop("exclude", None) or ()), 

193 ], 

194 dry_run=do_dry_run, 

195 **sync_kwargs, 

196 ) 

197 else: 

198 SYNC_LOGGER.log( 

199 IMPORTANT, 

200 f"{prefix} to push changes" 

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

202 ) 

203 local_chest = fs.ender_chest_folder(minecraft_root) 

204 push( 

205 local_chest, 

206 remote_uri, 

207 exclude=[ 

208 *this_chest.do_not_sync, 

209 *(sync_kwargs.pop("exclude", None) or ()), 

210 ], 

211 dry_run=do_dry_run, 

212 **sync_kwargs, 

213 ) 

214 except ( 

215 FileNotFoundError, 

216 ValueError, 

217 NotImplementedError, 

218 TimeoutError, 

219 RuntimeError, 

220 ) as sync_fail: 

221 SYNC_LOGGER.warning( 

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

223 f"\n {sync_fail}" 

224 ) 

225 break 

226 if do_dry_run == runs[-1]: 

227 continue 

228 if sync_confirm_wait is True: 

229 if not confirm(default=True): 

230 SYNC_LOGGER.error("Aborting") 

231 return 

232 else: 

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

234 sleep(sync_confirm_wait) 

235 else: 

236 synced_somewhere = True 

237 if pull_or_push == "pull": 

238 if this_chest.place_after_open and not dry_run: 

239 place.place_ender_chest( 

240 minecraft_root, 

241 keep_broken_links=False, 

242 keep_stale_links=False, 

243 error_handling="abort", 

244 relative=False, 

245 ) 

246 break 

247 if not synced_somewhere: 

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