Coverage for enderchest/inventory.py: 82%
116 statements
« prev ^ index » next coverage.py v7.5.0, created at 2024-05-04 01:41 +0000
« 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"""
3import logging
4from pathlib import Path
5from typing import Iterable, Sequence
6from urllib.parse import ParseResult
8from enderchest.sync import render_remote
10from . import EnderChest, InstanceSpec, ShulkerBox
11from . import filesystem as fs
12from .loggers import INVENTORY_LOGGER
15def load_ender_chest(minecraft_root: Path) -> EnderChest:
16 """Load the configuration from the enderchest.cfg file in the EnderChest
17 folder.
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)
25 Returns
26 -------
27 EnderChest
28 The EnderChest configuration
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
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
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.
62 Returns
63 -------
64 list of InstanceSpec
65 The instances registered with the EnderChest
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
99def render_instance(instance: InstanceSpec) -> str:
100 """Render an instance spec to a descriptive string
102 Parameters
103 ----------
104 instance : InstanceSpec
105 The instance spec to render
107 Returns
108 -------
109 str
110 {instance.name} ({instance.root})
111 """
112 return f"{instance.name} ({instance.root})"
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.
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.
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
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] = []
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 )
154 except FileNotFoundError:
155 INVENTORY_LOGGER.error(
156 f"There is no EnderChest installed within {minecraft_root}"
157 )
158 return []
160 shulker_boxes = sorted(shulker_boxes)
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
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 )
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.
193 Parameters
194 ----------
195 config_file : Path
196 Path to the config file
198 Returns
199 -------
200 ShulkerBox | None
201 The parsed shulker box or None, if the shulker box couldn't be parsed
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
216def _render_shulker_box(shulker_box: ShulkerBox) -> str:
217 """Render a shulker box to a descriptive string
219 Parameters
220 ----------
221 shulker_box : ShulkerBox
222 The shulker box spec to render
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
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
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.
253 Returns
254 -------
255 list of (URI, str) tuples
256 The URIs of the remote EnderChests, paired with their aliases
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 = ()
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 []
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
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
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
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 []
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 ]
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 )
340 INVENTORY_LOGGER.info(f"The instance {render_instance(mc)} {report}")
342 return matches
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
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
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 []
376 chest = load_ender_chest(minecraft_root)
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 []
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 []
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 )
403 matches = [
404 instance for instance in chest.instances if shulker_box.matches(instance)
405 ]
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 )
414 INVENTORY_LOGGER.info(
415 f"The shulker box {_render_shulker_box(shulker_box)} {report}"
416 )
418 return matches