Coverage for enderchest/inventory.py: 82%
117 statements
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-30 12:06 +0000
« prev ^ index » next coverage.py v7.10.1, created at 2025-07-30 12:06 +0000
1"""Functionality for resolving EnderChest and shulker box states"""
3import logging
4from collections.abc import Iterable, Sequence
5from pathlib import Path
6from typing import Any
7from urllib.parse import ParseResult
9from enderchest.sync import render_remote
11from . import EnderChest, InstanceSpec, ShulkerBox
12from . import filesystem as fs
13from .loggers import INVENTORY_LOGGER
16def load_ender_chest(minecraft_root: Path) -> EnderChest:
17 """Load the configuration from the enderchest.cfg file in the EnderChest
18 folder.
20 Parameters
21 ----------
22 minecraft_root : Path
23 The root directory that your minecraft stuff (or, at least, the one
24 that's the parent of your EnderChest folder)
26 Returns
27 -------
28 EnderChest
29 The EnderChest configuration
31 Raises
32 ------
33 FileNotFoundError
34 If no EnderChest folder exists in the given minecraft root or if no
35 enderchest.cfg file exists within that EnderChest folder
36 ValueError
37 If the EnderChest configuration is invalid and could not be parsed
38 """
39 config_path = fs.ender_chest_config(minecraft_root)
40 INVENTORY_LOGGER.debug("Loading %s", config_path)
41 ender_chest = EnderChest.from_cfg(config_path)
42 INVENTORY_LOGGER.debug("Parsed EnderChest installation from %s", minecraft_root)
43 return ender_chest
46def load_ender_chest_instances(
47 minecraft_root: Path, log_level: int = logging.INFO
48) -> Sequence[InstanceSpec]:
49 """Get the list of instances registered with the EnderChest located in the
50 minecraft root
52 Parameters
53 ----------
54 minecraft_root : Path
55 The root directory that your minecraft stuff (or, at least, the one
56 that's the parent of your EnderChest folder)
57 log_level : int, optional
58 By default, this method will report out the minecraft instances it
59 finds at the INFO level. You can optionally pass in a lower (or higher)
60 level if this method is being called from another method where that
61 information is redundant or overly verbose.
63 Returns
64 -------
65 list of InstanceSpec
66 The instances registered with the EnderChest
68 Notes
69 -----
70 If no EnderChest is installed in the given location, then this will return
71 an empty list rather than failing outright.
72 """
73 try:
74 ender_chest = load_ender_chest(minecraft_root)
75 instances: Sequence[InstanceSpec] = ender_chest.instances
76 except (FileNotFoundError, ValueError) as bad_chest:
77 INVENTORY_LOGGER.error(
78 "Could not load EnderChest from %s:\n %s", minecraft_root, bad_chest
79 )
80 instances = []
81 if len(instances) == 0:
82 INVENTORY_LOGGER.warning(
83 "There are no instances registered to the %s EnderChest", minecraft_root
84 )
85 else:
86 INVENTORY_LOGGER.log(
87 log_level,
88 "These are the instances that are currently registered"
89 " to the %s EnderChest:\n%s",
90 minecraft_root,
91 "\n".join(
92 [
93 f" {i + 1}. {render_instance(instance)}"
94 for i, instance in enumerate(instances)
95 ]
96 ),
97 )
98 return instances
101def render_instance(instance: InstanceSpec) -> str:
102 """Render an instance spec to a descriptive string
104 Parameters
105 ----------
106 instance : InstanceSpec
107 The instance spec to render
109 Returns
110 -------
111 str
112 {instance.name} ({instance.root})
113 """
114 return f"{instance.name} ({instance.root})"
117def load_shulker_boxes(
118 minecraft_root: Path, log_level: int = logging.INFO
119) -> list[ShulkerBox]:
120 """Load all shulker boxes in the EnderChest folder and return them in the
121 order in which they should be linked.
123 Parameters
124 ----------
125 minecraft_root : Path
126 The root directory that your minecraft stuff (or, at least, the one
127 that's the parent of your EnderChest folder)
128 log_level : int, optional
129 By default, this method will report out the minecraft instances it
130 finds at the INFO level. You can optionally pass in a lower (or higher)
131 level if this method is being called from another method where that
132 information is redundant or overly verbose.
134 Returns
135 -------
136 list of ShulkerBoxes
137 The shulker boxes found in the EnderChest folder, ordered in terms of
138 the sequence in which they should be linked
140 Notes
141 -----
142 If no EnderChest is installed in the given location, then this will return
143 an empty list rather than failing outright.
144 """
145 shulker_boxes: list[ShulkerBox] = []
147 try:
148 for shulker_config in fs.shulker_box_configs(minecraft_root):
149 try:
150 shulker_boxes.append(_load_shulker_box(shulker_config))
151 except (FileNotFoundError, ValueError) as bad_shulker:
152 INVENTORY_LOGGER.warning(
153 "%s\n Skipping shulker box %s",
154 bad_shulker,
155 shulker_config.parent.name,
156 )
158 except FileNotFoundError:
159 INVENTORY_LOGGER.error(
160 "There is no EnderChest installed within %s",
161 minecraft_root,
162 )
163 return []
165 shulker_boxes = sorted(shulker_boxes)
167 if len(shulker_boxes) == 0:
168 if log_level >= logging.INFO:
169 INVENTORY_LOGGER.warning(
170 "There are no shulker boxes within the %s EnderChest", minecraft_root
171 )
172 else:
173 report_shulker_boxes(
174 shulker_boxes, log_level, f"the {minecraft_root} EnderChest"
175 )
176 return shulker_boxes
179def report_shulker_boxes(
180 shulker_boxes: Iterable[ShulkerBox], log_level: int, ender_chest_name: str
181) -> None:
182 """Log the list of shulker boxes in the order they'll be linked
184 Parameters
185 ----------
186 shulker_boxes : list of ShulkerBoxes
187 The shulker boxes to report on
188 log_level : int
189 The log level of the report
190 ender_chest_name : str
191 Which chest is this?
193 Returns
194 -------
195 None
196 """
197 # TODO: properly log minecraft_root when one appears in the message
198 INVENTORY_LOGGER.log(
199 log_level,
200 f"These are the shulker boxes within {ender_chest_name}"
201 "\nlisted in the order in which they are linked:\n%s",
202 "\n".join(
203 f" {shulker_box.priority}. {_render_shulker_box(shulker_box)}"
204 for shulker_box in shulker_boxes
205 ),
206 )
209def _load_shulker_box(config_file: Path) -> ShulkerBox:
210 """Attempt to load a shulker box from a config file, and if you can't,
211 at least log why the loading failed.
213 Parameters
214 ----------
215 config_file : Path
216 Path to the config file
218 Returns
219 -------
220 ShulkerBox | None
221 The parsed shulker box or None, if the shulker box couldn't be parsed
223 Raises
224 ------
225 FileNotFoundError
226 If the given config file could not be found
227 ValueError
228 If there was a problem parsing the config file
229 """
230 INVENTORY_LOGGER.debug("Attempting to parse %s", config_file)
231 shulker_box = ShulkerBox.from_cfg(config_file)
232 INVENTORY_LOGGER.debug("Successfully parsed %s", _render_shulker_box(shulker_box))
233 return shulker_box
236def _render_shulker_box(shulker_box: ShulkerBox) -> str:
237 """Render a shulker box to a descriptive string
239 Parameters
240 ----------
241 shulker_box : ShulkerBox
242 The shulker box spec to render
244 Returns
245 -------
246 str
247 {priority}. {folder_name} [({name})]
248 (if different from folder name)
249 """
250 stringified = f"{shulker_box.root.name}"
251 if shulker_box.root.name != shulker_box.name: # pragma: no cover
252 # note: this is not a thing
253 stringified += f" ({shulker_box.name})"
254 return stringified
257def load_ender_chest_remotes(
258 minecraft_root: Path, log_level: int = logging.INFO
259) -> list[tuple[ParseResult, str]]:
260 """Load all remote EnderChest installations registered with this one
262 Parameters
263 ----------
264 minecraft_root : Path
265 The root directory that your minecraft stuff (or, at least, the one
266 that's the parent of your EnderChest folder)
267 log_level : int, optional
268 By default, this method will report out the minecraft instances it
269 finds at the INFO level. You can optionally pass in a lower (or higher)
270 level if this method is being called from another method where that
271 information is redundant or overly verbose.
273 Returns
274 -------
275 list of (URI, str) tuples
276 The URIs of the remote EnderChests, paired with their aliases
278 Notes
279 -----
280 If no EnderChest is installed in the given location, then this will return
281 an empty list rather than failing outright.
282 """
283 try:
284 ender_chest = load_ender_chest(minecraft_root)
285 remotes: Sequence[tuple[ParseResult, str]] = ender_chest.remotes
286 except (FileNotFoundError, ValueError) as bad_chest:
287 INVENTORY_LOGGER.error(
288 "Could not load EnderChest from %s:\n %s", minecraft_root, bad_chest
289 )
290 remotes = ()
292 if len(remotes) == 0:
293 if log_level >= logging.INFO:
294 INVENTORY_LOGGER.warning(
295 "There are no remotes registered to the %s EnderChest", minecraft_root
296 )
297 return []
299 report = (
300 "These are the remote EnderChest installations registered"
301 " to the one installed at %s"
302 )
303 remote_list: list[tuple[ParseResult, str]] = []
304 log_args: list[Any] = [minecraft_root]
305 for remote, alias in remotes:
306 report += "\n - %s"
307 log_args.append(render_remote(alias, remote))
308 remote_list.append((remote, alias))
309 INVENTORY_LOGGER.log(log_level, report, *log_args)
310 return remote_list
313def get_shulker_boxes_matching_instance(
314 minecraft_root: Path, instance_name: str
315) -> list[ShulkerBox]:
316 """Get the list of shulker boxes that the specified instance links to
318 Parameters
319 ----------
320 minecraft_root : Path
321 The root directory that your minecraft stuff (or, at least, the one
322 that's the parent of your EnderChest folder)
323 instance_name : str
324 The name of the instance you're asking about
326 Returns
327 -------
328 list of ShulkerBox
329 The shulker boxes that are linked to by the specified instance
330 """
331 try:
332 chest = load_ender_chest(minecraft_root)
333 except (FileNotFoundError, ValueError) as bad_chest:
334 INVENTORY_LOGGER.error(
335 "Could not load EnderChest from %s:\n %s", minecraft_root, bad_chest
336 )
337 return []
338 for mc in chest.instances:
339 if mc.name == instance_name:
340 break
341 else:
342 INVENTORY_LOGGER.error(
343 "No instance named %s is registered to this EnderChest", instance_name
344 )
345 return []
347 matches = [
348 box
349 for box in load_shulker_boxes(minecraft_root, log_level=logging.DEBUG)
350 if box.matches(mc) and box.matches_host(chest.name)
351 ]
353 if len(matches) == 0:
354 report = "does not link to any shulker boxes in this chest"
355 else:
356 # TODO: render as args
357 report = "links to the following shulker boxes:\n" + "\n".join(
358 f" - {_render_shulker_box(box)}" for box in matches
359 )
361 INVENTORY_LOGGER.info("The instance %s %s", render_instance(mc), report)
363 return matches
366def get_instances_matching_shulker_box(
367 minecraft_root: Path, shulker_box_name: str
368) -> list[InstanceSpec]:
369 """Get the list of registered instances that link to the specified shulker box
371 Parameters
372 ----------
373 minecraft_root : Path
374 The root directory that your minecraft stuff (or, at least, the one
375 that's the parent of your EnderChest folder)
376 shulker_box_name : str
377 The name of the shulker box you're asking about
379 Returns
380 -------
381 list of InstanceSpec
382 The instances that are / should be linked to the specified shulker box
383 """
384 try:
385 config_file = fs.shulker_box_config(minecraft_root, shulker_box_name)
386 except FileNotFoundError:
387 INVENTORY_LOGGER.error("No EnderChest is installed in %s", minecraft_root)
388 return []
389 try:
390 shulker_box = _load_shulker_box(config_file)
391 except (FileNotFoundError, ValueError) as bad_box:
392 INVENTORY_LOGGER.error(
393 "Could not load shulker box %s\n %s", shulker_box_name, bad_box
394 )
395 return []
397 chest = load_ender_chest(minecraft_root)
399 if not shulker_box.matches_host(chest.name):
400 INVENTORY_LOGGER.warning(
401 "This shulker box will not link to any instances on this machine"
402 )
403 return []
405 if not chest.instances:
406 INVENTORY_LOGGER.warning(
407 "This EnderChest does not have any instances registered."
408 " To register some, run the command:"
409 "\nenderchest gather minecraft",
410 )
411 return []
413 INVENTORY_LOGGER.debug(
414 "These are the instances that are currently registered"
415 " to the %s EnderChest:\n%s",
416 minecraft_root,
417 "\n".join(
418 [
419 f" {i + 1}. {render_instance(instance)}"
420 for i, instance in enumerate(chest.instances)
421 ]
422 ),
423 )
425 matches = [
426 instance for instance in chest.instances if shulker_box.matches(instance)
427 ]
429 if len(matches) == 0:
430 report = "is not linked to by any registered instances"
431 else:
432 report = "is linked to by the following instances:\n" + "\n".join(
433 f" - {render_instance(instance)}" for instance in matches
434 )
436 INVENTORY_LOGGER.info(
437 "The shulker box %s %s", _render_shulker_box(shulker_box), report
438 )
440 return matches