Coverage for enderchest/inventory.py: 82%

116 statements  

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

1"""Functionality for resolving EnderChest and shulker box states""" 

2 

3import logging 

4from pathlib import Path 

5from typing import Iterable, Sequence 

6from urllib.parse import ParseResult 

7 

8from enderchest.sync import render_remote 

9 

10from . import EnderChest, InstanceSpec, ShulkerBox 

11from . import filesystem as fs 

12from .loggers import INVENTORY_LOGGER 

13 

14 

15def load_ender_chest(minecraft_root: Path) -> EnderChest: 

16 """Load the configuration from the enderchest.cfg file in the EnderChest 

17 folder. 

18 

19 Parameters 

20 ---------- 

21 minecraft_root : Path 

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

23 that's the parent of your EnderChest folder) 

24 

25 Returns 

26 ------- 

27 EnderChest 

28 The EnderChest configuration 

29 

30 Raises 

31 ------ 

32 FileNotFoundError 

33 If no EnderChest folder exists in the given minecraft root or if no 

34 enderchest.cfg file exists within that EnderChest folder 

35 ValueError 

36 If the EnderChest configuration is invalid and could not be parsed 

37 """ 

38 config_path = fs.ender_chest_config(minecraft_root) 

39 INVENTORY_LOGGER.debug(f"Loading {config_path}") 

40 ender_chest = EnderChest.from_cfg(config_path) 

41 INVENTORY_LOGGER.debug(f"Parsed EnderChest installation from {minecraft_root}") 

42 return ender_chest 

43 

44 

45def load_ender_chest_instances( 

46 minecraft_root: Path, log_level: int = logging.INFO 

47) -> Sequence[InstanceSpec]: 

48 """Get the list of instances registered with the EnderChest located in the 

49 minecraft root 

50 

51 Parameters 

52 ---------- 

53 minecraft_root : Path 

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

55 that's the parent of your EnderChest folder) 

56 log_level : int, optional 

57 By default, this method will report out the minecraft instances it 

58 finds at the INFO level. You can optionally pass in a lower (or higher) 

59 level if this method is being called from another method where that 

60 information is redundant or overly verbose. 

61 

62 Returns 

63 ------- 

64 list of InstanceSpec 

65 The instances registered with the EnderChest 

66 

67 Notes 

68 ----- 

69 If no EnderChest is installed in the given location, then this will return 

70 an empty list rather than failing outright. 

71 """ 

72 try: 

73 ender_chest = load_ender_chest(minecraft_root) 

74 instances: Sequence[InstanceSpec] = ender_chest.instances 

75 except (FileNotFoundError, ValueError) as bad_chest: 

76 INVENTORY_LOGGER.error( 

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

78 ) 

79 instances = [] 

80 if len(instances) == 0: 

81 INVENTORY_LOGGER.warning( 

82 f"There are no instances registered to the {minecraft_root} EnderChest", 

83 ) 

84 else: 

85 INVENTORY_LOGGER.log( 

86 log_level, 

87 "These are the instances that are currently registered" 

88 f" to the {minecraft_root} EnderChest:\n%s", 

89 "\n".join( 

90 [ 

91 f" {i + 1}. {render_instance(instance)}" 

92 for i, instance in enumerate(instances) 

93 ] 

94 ), 

95 ) 

96 return instances 

97 

98 

99def render_instance(instance: InstanceSpec) -> str: 

100 """Render an instance spec to a descriptive string 

101 

102 Parameters 

103 ---------- 

104 instance : InstanceSpec 

105 The instance spec to render 

106 

107 Returns 

108 ------- 

109 str 

110 {instance.name} ({instance.root}) 

111 """ 

112 return f"{instance.name} ({instance.root})" 

113 

114 

115def load_shulker_boxes( 

116 minecraft_root: Path, log_level: int = logging.INFO 

117) -> list[ShulkerBox]: 

118 """Load all shulker boxes in the EnderChest folder and return them in the 

119 order in which they should be linked. 

120 

121 Parameters 

122 ---------- 

123 minecraft_root : Path 

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

125 that's the parent of your EnderChest folder) 

126 log_level : int, optional 

127 By default, this method will report out the minecraft instances it 

128 finds at the INFO level. You can optionally pass in a lower (or higher) 

129 level if this method is being called from another method where that 

130 information is redundant or overly verbose. 

131 

132 Returns 

133 ------- 

134 list of ShulkerBoxes 

135 The shulker boxes found in the EnderChest folder, ordered in terms of 

136 the sequence in which they should be linked 

137 

138 Notes 

139 ----- 

140 If no EnderChest is installed in the given location, then this will return 

141 an empty list rather than failing outright. 

142 """ 

143 shulker_boxes: list[ShulkerBox] = [] 

144 

145 try: 

146 for shulker_config in fs.shulker_box_configs(minecraft_root): 

147 try: 

148 shulker_boxes.append(_load_shulker_box(shulker_config)) 

149 except (FileNotFoundError, ValueError) as bad_shulker: 

150 INVENTORY_LOGGER.warning( 

151 f"{bad_shulker}\n Skipping shulker box {shulker_config.parent.name}" 

152 ) 

153 

154 except FileNotFoundError: 

155 INVENTORY_LOGGER.error( 

156 f"There is no EnderChest installed within {minecraft_root}" 

157 ) 

158 return [] 

159 

160 shulker_boxes = sorted(shulker_boxes) 

161 

162 if len(shulker_boxes) == 0: 

163 if log_level >= logging.INFO: 

164 INVENTORY_LOGGER.warning( 

165 f"There are no shulker boxes within the {minecraft_root} EnderChest" 

166 ) 

167 else: 

168 report_shulker_boxes( 

169 shulker_boxes, log_level, f"the {minecraft_root} EnderChest" 

170 ) 

171 return shulker_boxes 

172 

173 

174def report_shulker_boxes( 

175 shulker_boxes: Iterable[ShulkerBox], log_level: int, ender_chest_name: str 

176) -> None: 

177 """Log the list of shulker boxes in the order they'll be linked""" 

178 INVENTORY_LOGGER.log( 

179 log_level, 

180 f"These are the shulker boxes within {ender_chest_name}" 

181 "\nlisted in the order in which they are linked:\n%s", 

182 "\n".join( 

183 f" {shulker_box.priority}. {_render_shulker_box(shulker_box)}" 

184 for shulker_box in shulker_boxes 

185 ), 

186 ) 

187 

188 

189def _load_shulker_box(config_file: Path) -> ShulkerBox: 

190 """Attempt to load a shulker box from a config file, and if you can't, 

191 at least log why the loading failed. 

192 

193 Parameters 

194 ---------- 

195 config_file : Path 

196 Path to the config file 

197 

198 Returns 

199 ------- 

200 ShulkerBox | None 

201 The parsed shulker box or None, if the shulker box couldn't be parsed 

202 

203 Raises 

204 ------ 

205 FileNotFoundError 

206 If the given config file could not be found 

207 ValueError 

208 If there was a problem parsing the config file 

209 """ 

210 INVENTORY_LOGGER.debug(f"Attempting to parse {config_file}") 

211 shulker_box = ShulkerBox.from_cfg(config_file) 

212 INVENTORY_LOGGER.debug(f"Successfully parsed {_render_shulker_box(shulker_box)}") 

213 return shulker_box 

214 

215 

216def _render_shulker_box(shulker_box: ShulkerBox) -> str: 

217 """Render a shulker box to a descriptive string 

218 

219 Parameters 

220 ---------- 

221 shulker_box : ShulkerBox 

222 The shulker box spec to render 

223 

224 Returns 

225 ------- 

226 str 

227 {priority}. {folder_name} [({name})] 

228 (if different from folder name) 

229 """ 

230 stringified = f"{shulker_box.root.name}" 

231 if shulker_box.root.name != shulker_box.name: # pragma: no cover 

232 # note: this is not a thing 

233 stringified += f" ({shulker_box.name})" 

234 return stringified 

235 

236 

237def load_ender_chest_remotes( 

238 minecraft_root: Path, log_level: int = logging.INFO 

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

240 """Load all remote EnderChest installations registered with this one 

241 

242 Parameters 

243 ---------- 

244 minecraft_root : Path 

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

246 that's the parent of your EnderChest folder) 

247 log_level : int, optional 

248 By default, this method will report out the minecraft instances it 

249 finds at the INFO level. You can optionally pass in a lower (or higher) 

250 level if this method is being called from another method where that 

251 information is redundant or overly verbose. 

252 

253 Returns 

254 ------- 

255 list of (URI, str) tuples 

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

257 

258 Notes 

259 ----- 

260 If no EnderChest is installed in the given location, then this will return 

261 an empty list rather than failing outright. 

262 """ 

263 try: 

264 ender_chest = load_ender_chest(minecraft_root) 

265 remotes: Sequence[tuple[ParseResult, str]] = ender_chest.remotes 

266 except (FileNotFoundError, ValueError) as bad_chest: 

267 INVENTORY_LOGGER.error( 

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

269 ) 

270 remotes = () 

271 

272 if len(remotes) == 0: 

273 if log_level >= logging.INFO: 

274 INVENTORY_LOGGER.warning( 

275 f"There are no remotes registered to the {minecraft_root} EnderChest" 

276 ) 

277 return [] 

278 

279 report = ( 

280 "These are the remote EnderChest installations registered" 

281 f" to the one installed at {minecraft_root}" 

282 ) 

283 remote_list: list[tuple[ParseResult, str]] = [] 

284 log_args: list[str] = [] 

285 for remote, alias in remotes: 

286 report += "\n - %s" 

287 log_args.append(render_remote(alias, remote)) 

288 remote_list.append((remote, alias)) 

289 INVENTORY_LOGGER.log(log_level, report, *log_args) 

290 return remote_list 

291 

292 

293def get_shulker_boxes_matching_instance( 

294 minecraft_root: Path, instance_name: str 

295) -> list[ShulkerBox]: 

296 """Get the list of shulker boxes that the specified instance links to 

297 

298 Parameters 

299 ---------- 

300 minecraft_root : Path 

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

302 that's the parent of your EnderChest folder) 

303 instance_name : str 

304 The name of the instance you're asking about 

305 

306 Returns 

307 ------- 

308 list of ShulkerBox 

309 The shulker boxes that are linked to by the specified instance 

310 """ 

311 try: 

312 chest = load_ender_chest(minecraft_root) 

313 except (FileNotFoundError, ValueError) as bad_chest: 

314 INVENTORY_LOGGER.error( 

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

316 ) 

317 return [] 

318 for mc in chest.instances: 

319 if mc.name == instance_name: 

320 break 

321 else: 

322 INVENTORY_LOGGER.error( 

323 "No instance named %s is registered to this EnderChest", instance_name 

324 ) 

325 return [] 

326 

327 matches = [ 

328 box 

329 for box in load_shulker_boxes(minecraft_root, log_level=logging.DEBUG) 

330 if box.matches(mc) and box.matches_host(chest.name) 

331 ] 

332 

333 if len(matches) == 0: 

334 report = "does not link to any shulker boxes in this chest" 

335 else: 

336 report = "links to the following shulker boxes:\n" + "\n".join( 

337 f" - {_render_shulker_box(box)}" for box in matches 

338 ) 

339 

340 INVENTORY_LOGGER.info(f"The instance {render_instance(mc)} {report}") 

341 

342 return matches 

343 

344 

345def get_instances_matching_shulker_box( 

346 minecraft_root: Path, shulker_box_name: str 

347) -> list[InstanceSpec]: 

348 """Get the list of registered instances that link to the specified shulker box 

349 

350 Parameters 

351 ---------- 

352 minecraft_root : Path 

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

354 that's the parent of your EnderChest folder) 

355 shulker_box_name : str 

356 The name of the shulker box you're asking about 

357 

358 Returns 

359 ------- 

360 list of InstanceSpec 

361 The instances that are / should be linked to the specified shulker box 

362 """ 

363 try: 

364 config_file = fs.shulker_box_config(minecraft_root, shulker_box_name) 

365 except FileNotFoundError: 

366 INVENTORY_LOGGER.error(f"No EnderChest is installed in {minecraft_root}") 

367 return [] 

368 try: 

369 shulker_box = _load_shulker_box(config_file) 

370 except (FileNotFoundError, ValueError) as bad_box: 

371 INVENTORY_LOGGER.error( 

372 f"Could not load shulker box {shulker_box_name}\n {bad_box}" 

373 ) 

374 return [] 

375 

376 chest = load_ender_chest(minecraft_root) 

377 

378 if not shulker_box.matches_host(chest.name): 

379 INVENTORY_LOGGER.warning( 

380 "This shulker box will not link to any instances on this machine" 

381 ) 

382 return [] 

383 

384 if not chest.instances: 

385 INVENTORY_LOGGER.warning( 

386 "This EnderChest does not have any instances registered." 

387 " To register some, run the command:" 

388 "\nenderchest gather minecraft", 

389 ) 

390 return [] 

391 

392 INVENTORY_LOGGER.debug( 

393 "These are the instances that are currently registered" 

394 f" to the {minecraft_root} EnderChest:\n%s", 

395 "\n".join( 

396 [ 

397 f" {i + 1}. {render_instance(instance)}" 

398 for i, instance in enumerate(chest.instances) 

399 ] 

400 ), 

401 ) 

402 

403 matches = [ 

404 instance for instance in chest.instances if shulker_box.matches(instance) 

405 ] 

406 

407 if len(matches) == 0: 

408 report = "is not linked to by any registered instances" 

409 else: 

410 report = "is linked to by the following instances:\n" + "\n".join( 

411 f" - {render_instance(instance)}" for instance in matches 

412 ) 

413 

414 INVENTORY_LOGGER.info( 

415 f"The shulker box {_render_shulker_box(shulker_box)} {report}" 

416 ) 

417 

418 return matches