Coverage for enderchest/remote.py: 80%
82 statements
« prev ^ index » next coverage.py v7.4.0, created at 2024-01-11 17:09 +0000
« 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
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
15def load_remote_ender_chest(uri: str | ParseResult) -> EnderChest:
16 """Load an EnderChest configuration from another machine
18 Parameters
19 ----------
20 uri : URI
21 The URI to the remote Minecraft root
23 Returns
24 -------
25 EnderChest
26 The remote EnderChest configuration
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
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())
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
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
59 Parameters
60 ----------
61 uri : URI
62 The URI to the remote Minecraft root
64 Returns
65 -------
66 list of (URI, str) tuples
67 The URIs of the remote EnderChests, paired with their aliases
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 ]
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 )
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
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
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
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.
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)
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
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")