redfetch 1.3.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,62 @@
1
+ """Writes update_status.json after `redfetch check` completes."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from datetime import datetime, timezone
7
+ from typing import Literal
8
+
9
+ from redfetch import config
10
+ from redfetch.sync_types import ExecutionPlan
11
+
12
+ SCHEMA_VERSION = 1
13
+ UPDATE_STATUS_FILENAME = "update_status.json"
14
+
15
+ AuthState = Literal["ok", "needs_login", "not_configured"]
16
+
17
+
18
+ def update_status_path() -> str:
19
+ """Location beside last_command.json so external apps need only one path."""
20
+ return os.path.join(config.DEFAULT_CONFIG_DIR, UPDATE_STATUS_FILENAME)
21
+
22
+
23
+ def build_items_from_plan(execution_plan: ExecutionPlan) -> list[dict]:
24
+ """Only stuff we're actually going to download (opt-outs, blocks, and new installs aren't in the plan)."""
25
+ items: list[dict] = []
26
+ for action in execution_plan.actions.values():
27
+ if action.action != "download" or action.reason != "outdated":
28
+ continue
29
+ items.append(
30
+ {
31
+ "resource_id": action.resource_id,
32
+ "name": action.title or action.resource_id,
33
+ "available_version_id": action.remote_version,
34
+ }
35
+ )
36
+ return items
37
+
38
+
39
+ def write_update_status(
40
+ *,
41
+ env: str,
42
+ auth_state: AuthState,
43
+ items: list[dict] | None = None,
44
+ managed_path: str | None = None,
45
+ ) -> dict:
46
+ """Write update_status.json and return the payload. Only auth_state "ok" carries items."""
47
+ items = items or []
48
+ if auth_state != "ok":
49
+ items = []
50
+
51
+ payload = {
52
+ "schema_version": SCHEMA_VERSION,
53
+ "checked_at": int(datetime.now(timezone.utc).timestamp()),
54
+ "env": env.upper(),
55
+ "auth_state": auth_state,
56
+ "managed_path": managed_path,
57
+ "updates": {
58
+ "items": items,
59
+ },
60
+ }
61
+ config.atomic_write_json(update_status_path(), payload)
62
+ return payload
redfetch/utils.py ADDED
@@ -0,0 +1,256 @@
1
+ """Miscellaneous helpers, mostly path resolution and URL parsing."""
2
+
3
+ # Standard
4
+ import os
5
+ import re
6
+ import shlex
7
+ import sys
8
+ from urllib.parse import urlparse
9
+
10
+ # Local
11
+ from redfetch import config
12
+
13
+ #
14
+ # path functions
15
+ #
16
+
17
+ def get_base_path() -> str:
18
+ """Determine the base path based on the active version."""
19
+ return get_vvmq_path() or get_current_download_folder()
20
+
21
+
22
+ def resolve_special_destination(special_resource: dict | None, download_folder: str) -> str | None:
23
+ """Resolve a special resource destination path without side effects."""
24
+ if not special_resource:
25
+ return None
26
+ custom_path = special_resource.get("custom_path")
27
+ if custom_path:
28
+ return os.path.normpath(os.path.realpath(custom_path))
29
+ default_path = special_resource.get("default_path")
30
+ if default_path:
31
+ return os.path.normpath(os.path.join(download_folder, default_path))
32
+ return None
33
+
34
+
35
+ def _resolve_current_special_path(resource_id: str) -> str | None:
36
+ """Resolve the path for a special resource in the current environment."""
37
+ settings = config.settings.from_env(config.settings.ENV)
38
+ return resolve_special_destination(
39
+ settings.SPECIAL_RESOURCES.get(resource_id), settings.DOWNLOAD_FOLDER
40
+ )
41
+
42
+
43
+ def is_safe_path(base_directory: str, target_path: str) -> bool:
44
+ """Check for directory traversal."""
45
+ abs_base = os.path.realpath(base_directory)
46
+ abs_target = os.path.realpath(target_path)
47
+ return os.path.commonpath([abs_base, abs_target]) == abs_base
48
+
49
+
50
+ def get_current_vvmq_id(settings_env: str | None = None) -> str | None:
51
+ env = (settings_env or config.settings.ENV).upper()
52
+ for resource_id, env_name in config.VANILLA_MAP.items():
53
+ if env_name.upper() == env:
54
+ return str(resource_id)
55
+ return None
56
+
57
+
58
+ def get_vvmq_path() -> str | None:
59
+ vvmq_id = get_current_vvmq_id()
60
+ if not vvmq_id:
61
+ return None
62
+ return _resolve_current_special_path(vvmq_id)
63
+
64
+
65
+ def get_current_myseq_id() -> str | None:
66
+ current_env = config.settings.ENV.upper()
67
+ for resource_id, env_name in config.MYSEQ_MAP.items():
68
+ if env_name.upper() == current_env:
69
+ return str(resource_id)
70
+ return None
71
+
72
+
73
+ def get_myseq_path() -> str | None:
74
+ myseq_id = get_current_myseq_id()
75
+ if not myseq_id:
76
+ return None
77
+ return _resolve_current_special_path(myseq_id)
78
+
79
+
80
+ def get_current_download_folder() -> str:
81
+ return os.path.normpath(config.settings.from_env(config.settings.ENV).DOWNLOAD_FOLDER)
82
+
83
+
84
+ def get_eq_maps_status() -> str | None:
85
+ """Get the status of EQ maps (Brewall's and Good's)."""
86
+ special_resources = config.settings.from_env(config.settings.ENV).SPECIAL_RESOURCES
87
+ brewall_opt_in = special_resources.get('153', {}).get('opt_in', False)
88
+ good_opt_in = special_resources.get('303', {}).get('opt_in', False)
89
+
90
+ if brewall_opt_in and good_opt_in:
91
+ return "all"
92
+ elif brewall_opt_in:
93
+ return "brewall"
94
+ elif good_opt_in:
95
+ return "good"
96
+ else:
97
+ return None
98
+
99
+
100
+ def parse_resource_id(input_string: str) -> str:
101
+ # Check if it's already a number
102
+ if input_string.isdigit():
103
+ return input_string
104
+
105
+ # Parse the URL
106
+ parsed_url = urlparse(input_string)
107
+
108
+ # Check if it's a redguides.com URL
109
+ if not parsed_url.netloc.endswith('redguides.com'):
110
+ print(f"Invalid URL: Neither a redguides.com URL nor a valid resource id")
111
+ raise ValueError("Invalid URL: Neither a redguides.com URL nor a valid resource id")
112
+
113
+ # Check if it's a thread URL
114
+ if 'threads' in parsed_url.path:
115
+ print(f"Invalid URL: This appears to be a discussion thread, not a resource")
116
+ raise ValueError("Invalid URL: This appears to be a discussion thread, not a resource")
117
+
118
+ # Extract the resource ID using regex
119
+ match = re.search(r'\.(\d+)(?:/|$)', parsed_url.path)
120
+ if match:
121
+ return str(match.group(1))
122
+ else:
123
+ print(f"Could not find a valid resource ID in the URL")
124
+ raise ValueError("Could not find a valid resource ID in the URL")
125
+
126
+
127
+ def validate_file_in_path(path: str | None, filename: str) -> bool:
128
+ """Validate that the given path contains a specific file."""
129
+ if not path:
130
+ return False
131
+ try:
132
+ return os.path.isfile(os.path.join(path, filename))
133
+ except (TypeError, ValueError):
134
+ return False
135
+
136
+
137
+ #
138
+ # post-update launch
139
+ #
140
+
141
+ # Presets offered as "Also start post-update" toggles.
142
+ POST_UPDATE_PRESETS = {
143
+ "eqbcs": (get_vvmq_path, "EQBCS.exe"),
144
+ "myseq": (get_myseq_path, "MySEQ.exe"),
145
+ }
146
+
147
+ POST_UPDATE_PRESET_LABELS = {
148
+ "eqbcs": "EQBCS",
149
+ "myseq": "MySEQ",
150
+ "custom": "Custom",
151
+ }
152
+
153
+
154
+ def post_update_launch_choices() -> list[tuple[str, str]]:
155
+ """Return ordered ``(value, label)`` toggle choices (presets + custom)."""
156
+ choices: list[tuple[str, str]] = [
157
+ (key, POST_UPDATE_PRESET_LABELS[key]) for key in POST_UPDATE_PRESETS
158
+ ]
159
+ choices.append(("custom", POST_UPDATE_PRESET_LABELS["custom"]))
160
+ return choices
161
+
162
+
163
+ def get_post_update_targets(env: str | None = None) -> list[str]:
164
+ """Return enabled post-update targets for ``env`` (``targets``, or legacy ``target``)."""
165
+ env = env or config.settings.ENV
166
+ cfg = config.settings.from_env(env).get("POST_UPDATE_LAUNCH", {})
167
+ raw = cfg.get("targets")
168
+ if raw is None:
169
+ single = cfg.get("target")
170
+ raw = [single] if single else []
171
+ elif isinstance(raw, str):
172
+ raw = [raw]
173
+
174
+ result: list[str] = []
175
+ for item in raw:
176
+ value = str(item).strip().lower()
177
+ if value and value != "none" and value not in result:
178
+ result.append(value)
179
+ return result
180
+
181
+
182
+ def _command_program(command: list[str] | str) -> str:
183
+ """Return the program (first token) of a command list or string."""
184
+ if isinstance(command, str):
185
+ s = command.strip()
186
+ if s.startswith('"'):
187
+ end = s.find('"', 1)
188
+ return s[1:end] if end != -1 else s[1:]
189
+ return s.split(None, 1)[0] if s else ""
190
+ return str(command[0]) if command else ""
191
+
192
+
193
+ def resolve_post_update_launch(
194
+ env: str | None = None,
195
+ ) -> list[tuple[list[str] | str, str | None]]:
196
+ """Resolve enabled targets for ``env`` to ``(command, cwd)`` pairs, skipping unresolvable ones."""
197
+ env = env or config.settings.ENV
198
+ resolved: list[tuple[list[str] | str, str | None]] = []
199
+ for target in get_post_update_targets(env):
200
+ item = _resolve_launch_target(target, env)
201
+ if item:
202
+ resolved.append(item)
203
+ return resolved
204
+
205
+
206
+ def _resolve_launch_target(
207
+ target: str,
208
+ env: str | None = None,
209
+ ) -> tuple[list[str] | str, str | None] | None:
210
+ """Resolve a single post-update ``target`` to a ``(command, cwd)`` pair."""
211
+ env = env or config.settings.ENV
212
+ cfg = config.settings.from_env(env).get("POST_UPDATE_LAUNCH", {})
213
+
214
+ if target == "custom":
215
+ command = cfg.get("command")
216
+ if not command:
217
+ print("Post-update launch is set to Custom, but no command is configured; skipping.")
218
+ return None
219
+
220
+ is_ps1 = (
221
+ sys.platform == "win32"
222
+ and _command_program(command).lower().endswith(".ps1")
223
+ )
224
+
225
+ if isinstance(command, str):
226
+ command = command.strip()
227
+ if not command:
228
+ print("Post-update launch command is empty; skipping.")
229
+ return None
230
+ if sys.platform == "win32":
231
+ if is_ps1:
232
+ command = (
233
+ "powershell -NoProfile -ExecutionPolicy Bypass -File " + command
234
+ )
235
+ return (command, None)
236
+ return (shlex.split(command, posix=True), None)
237
+
238
+ if not isinstance(command, (list, tuple)):
239
+ raise TypeError("POST_UPDATE_LAUNCH command must be a string or list.")
240
+ argv = [str(part) for part in command]
241
+ if not argv:
242
+ print("Post-update launch command is empty; skipping.")
243
+ return None
244
+ if is_ps1:
245
+ argv = ["powershell", "-NoProfile", "-ExecutionPolicy", "Bypass", "-File", *argv]
246
+ return (argv, None)
247
+
248
+ preset = POST_UPDATE_PRESETS.get(target)
249
+ if not preset:
250
+ print(f"Unknown POST_UPDATE_LAUNCH target: {target}; skipping.")
251
+ return None
252
+ resolver, exe = preset
253
+ folder = resolver()
254
+ if folder and validate_file_in_path(folder, exe):
255
+ return ([os.path.join(folder, exe)], folder)
256
+ return None
@@ -0,0 +1,257 @@
1
+ Metadata-Version: 2.4
2
+ Name: redfetch
3
+ Version: 1.3.0
4
+ Summary: Download and publish EverQuest scripts and software using the RedGuides API
5
+ Project-URL: Homepage, https://www.redguides.com
6
+ Project-URL: Documentation, https://www.redguides.com/community/resources/redfetch.3177/
7
+ Project-URL: Repository, https://github.com/RedGuides/redfetch
8
+ Project-URL: Issues, https://github.com/RedGuides/redfetch/issues
9
+ Project-URL: Changelog, https://github.com/RedGuides/redfetch/blob/main/CHANGELOG.md
10
+ Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/aaf2d42ab82d45abeadfafa943e2b61abba2cb60.zip
11
+ Author-email: Redbot <ask@redguides.com>
12
+ License-Expression: GPL-3.0-or-later
13
+ License-File: LICENSE
14
+ Classifier: Development Status :: 5 - Production/Stable
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)
17
+ Classifier: Operating System :: OS Independent
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.12
21
+ Requires-Dist: aiofiles~=25.1
22
+ Requires-Dist: aiohttp~=3.14
23
+ Requires-Dist: aiosqlite~=0.22
24
+ Requires-Dist: cachetools~=7.1
25
+ Requires-Dist: diskcache~=5.6
26
+ Requires-Dist: dynaconf<3.3,~=3.2
27
+ Requires-Dist: httpx[http2]~=0.28
28
+ Requires-Dist: keepachangelog~=2.0
29
+ Requires-Dist: keyring~=25.7
30
+ Requires-Dist: md2bbcode
31
+ Requires-Dist: packaging~=26.2
32
+ Requires-Dist: platformdirs~=4.10
33
+ Requires-Dist: psutil~=7.2
34
+ Requires-Dist: pydantic~=2.13
35
+ Requires-Dist: pyperclip~=1.11
36
+ Requires-Dist: pywin32; sys_platform == 'win32'
37
+ Requires-Dist: rich~=15.0
38
+ Requires-Dist: tenacity~=9.1
39
+ Requires-Dist: textual-fspicker~=1.0
40
+ Requires-Dist: textual~=8.2
41
+ Requires-Dist: tomlkit~=0.15
42
+ Requires-Dist: typer~=0.26
43
+ Description-Content-Type: text/markdown
44
+
45
+ ![six wizards levitating a package, the word redfetch underneath](https://www.redguides.com/images/redfetchlogo.png)
46
+
47
+ redfetch is for updating software and scripts for EverQuest that RedGuides recommends, as well as those you "[watch](https://www.redguides.com/community/watched/resources)". It's also open source, how nice.
48
+
49
+ ## Installation
50
+
51
+ On Windows the easiest way to install redfetch is to [download](https://www.redguides.com/community/resources/redfetch.3177/download) and run [`redfetch.exe`](https://www.redguides.com/community/resources/redfetch.3177/download). (*optional: If you're still on Windows 10 and want a more modern appearance, follow [this guide](https://www.redguides.com/community/threads/redfetch.92998/post-634938) to set [Windows Terminal](https://www.redguides.com/community/threads/redfetch.92998/post-634938) as your default terminal.*)
52
+
53
+ <details>
54
+ <summary>Terminal / Python / Linux</summary>
55
+
56
+
57
+ Make sure you have a recent version of [Python](https://www.python.org/downloads/)
58
+
59
+ 1) **Install pipx**
60
+ ```bash
61
+ python -m pip install --user pipx
62
+ ```
63
+
64
+ 2) **Make it so you can run packages without having to type "python -m"**
65
+ ```bash
66
+ python -m pipx ensurepath
67
+ ```
68
+
69
+ 3) **Install redfetch**
70
+ ```bash
71
+ pipx install redfetch
72
+ ```
73
+
74
+ When you open a new terminal window you'll be able to run redfetch by typing `redfetch` from the command line.
75
+
76
+ </details>
77
+
78
+ ## Usage
79
+
80
+
81
+ ### 1) Double-click [`redfetch.exe`](https://www.redguides.com/community/resources/redfetch.3177/download) to run the script.
82
+ Take a moment to consider your configuration and the settings tab.
83
+
84
+ ### 2) Click the big blue "Easy Update" button, and then "Yes" or "Always" on the popup.
85
+ ![a screenshot showing the easy update button](https://www.redguides.com/images/redfetchupdate.gif)
86
+ (It's updating *Very Vanilla MQ* and any of its scripts or plugins you have [watched on RedGuides](https://www.redguides.com/community/watched/resources), your licensed resources, and scripts recommended by staff. You can customize this if you like.)
87
+
88
+ Now you're ready to play EQ with the big boys.
89
+
90
+ ## Add more MQ Scripts
91
+ To add more MacroQuest scripts, "watch" them on [www.redguides.com/community/resources](https://www.redguides.com/community/resources), and then click the *Easy Update* button again.
92
+
93
+ ![a screenshot showing the watch button on a resource page](https://www.redguides.com/images/clickwatch.gif)
94
+
95
+ If there are non-MQ resources you'd like to keep in sync with redfetch, you can add them as a "special resource" in the local settings file, as shown in settings section.
96
+
97
+ ## Command Line
98
+ To run redfetch from the command line:
99
+
100
+ | .exe file | python |
101
+ |---------|-----------|
102
+ | `.\redfetch.exe update` | `redfetch update` |
103
+
104
+ ![a screenshot showing the command line interface](https://www.redguides.com/images/redfetchcliupdate.gif)
105
+
106
+ ## Command Line Reference
107
+
108
+ > Run `redfetch --help` or `.\redfetch.exe --help` to see something like this in your terminal:
109
+ >
110
+ > ### 📦 Resource Management
111
+ > - `update` - Update all watched and special resources
112
+ > - `--force` / `-f` - Force re-download of all watched resources
113
+ > - `--server` / `-s` - Switch to this server before updating (`LIVE`, `TEST`, `EMU`)
114
+ > - `download <ID_OR_URL>` - Download a specific resource by ID or URL
115
+ > - `--force` / `-f` - Force re-download by resetting this resource's download date
116
+ > - `--server` / `-s` - Switch to this server before downloading (`LIVE`, `TEST`, `EMU`)
117
+ > - `list` - List resources and dependencies in the cache database
118
+ > - `reset` - Reset download dates for watched resources
119
+ >
120
+ > ### 🍔 Configuration
121
+ > - `server <SERVER>` - Switch the current server/environment to `LIVE`, `TEST`, or `EMU`
122
+ > - `config <SETTING_PATH> <VALUE>` - Update a setting by path and value
123
+ > - `SETTING_PATH` - Dot-separated setting path (e.g., `SPECIAL_RESOURCES.1974.opt_in`)
124
+ > - `VALUE` - New value for the setting
125
+ > - `--server` / `-s` - Server to apply the change in (`LIVE`, `TEST`, `EMU`)
126
+ > - `status` - Show the configuration for the current or specified server
127
+ > - `--server` / `-s` - Server to show (defaults to current)
128
+ >
129
+ > ### 🔧 System & Utilities
130
+ > - `ui` - Launch the Terminal User Interface
131
+ > - `web` - Launch the RedGuides.com web interface
132
+ > - `version` - Show version and exit
133
+ > - `logout` - Disconnect your account from redfetch
134
+ > - `uninstall` - Uninstall redfetch and clean up data
135
+ >
136
+ > ### 📤 Publishing
137
+ > - `publish <resource_id>` - Publish an update to you or your team's resource. [There's also a github action for this.](https://github.com/marketplace/actions/redguides-publish)
138
+ > - `resource_id` - Existing RedGuides resource ID
139
+ > - `--description <README.md>` / `-d` - Path to a file (e.g. `README.md`) that will become the resource's overview description
140
+ > - `--version <version_number>` / `-v` - New version string (e.g., `v1.0.1`)
141
+ > - `--message <CHANGELOG.md | MESSAGE>` / `-m` - Version update message, a message file (e.g. `message.md` / `message.txt`), or a `CHANGELOG.md` (keep a changelog) file.
142
+ > - `--file <FILE.zip>` / `-f` - Path to your zipped release file
143
+ > - `--domain <URL>` - Domain to prepend to relative URLs in README.md or CHANGELOG.md files. (mostly for images. e.g., `https://raw.githubusercontent.com/yourusername/yourrepo/main/`)
144
+
145
+ ## Settings
146
+
147
+ `settings.local.toml` is found in your configuration directory, which by default is `c:\Users\Public\redfetch\settings.local.toml`. Any keys you add will override their default values in [`settings.toml`](./src/redfetch/settings.toml).
148
+
149
+ All settings are prefixed with the environment,
150
+
151
+ - `[DEFAULT]` - encompasses all environments that are not explicitly defined.
152
+ - `[LIVE]` - EverQuest Live
153
+ - `[TEST]` - EverQuest Test
154
+ - `[EMU]` - EverQuest Emulator
155
+
156
+ ### Adding a special resource
157
+ To add a "special resource" (a non-MQ resource that you want to keep updated), open `settings.local.toml` and add an entry. You'll need the [resource ID (numbers at the end of the url)](https://www.redguides.com/community/resources/brewalls-everquest-maps.153/) and a target directory. Example:
158
+
159
+ ```toml
160
+ [LIVE.SPECIAL_RESOURCES.153]
161
+ custom_path = 'C:\Users\Public\Daybreak Game Company\Installed Games\EverQuest\maps\Brewall_Maps'
162
+ opt_in = true
163
+ ```
164
+ * Note the use of single quotes around the path, which are required for windows paths.
165
+
166
+ The above will install Brewall's maps to the EQ maps directory the next time `--download-watched` is run for `LIVE` servers.
167
+
168
+ ### Overwrite protection
169
+
170
+ If there are local files you don't want overwritten by a resource, you can add them to the `PROTECTED_FILES_BY_RESOURCE` setting. Include the resource ID and files you want to protect. e.g.,
171
+
172
+ ```toml
173
+ [LIVE.PROTECTED_FILES_BY_RESOURCE]
174
+ 1974 = ["CharSelect.cfg", "Zoned.cfg", "MQ2Map.ini", "MQ2MoveUtils.ini"]
175
+ 153 = ["citymist.txt", "innothule.txt", "oasis.txt"]
176
+ navmesh = ["befallen.navmesh", "innothuleb.navmesh"]
177
+ ```
178
+
179
+ ### Custom category directories
180
+
181
+ If you share `lua`, `macros`, or `plugins` directories across multiple MQ environments, you can override where an entire category is installed. Add a `CATEGORY_PATHS` section to your `settings.local.toml`:
182
+
183
+ ```toml
184
+ [DEFAULT.CATEGORY_PATHS]
185
+ lua = 'D:\\shared\\lua'
186
+ macros = 'D:\\shared\\macros'
187
+ ```
188
+
189
+ Absolute paths are used as-is. Relative paths are joined to `DOWNLOAD_FOLDER`. You can set this globally in `[DEFAULT]` or per-environment (`[LIVE.CATEGORY_PATHS]`, `[TEST.CATEGORY_PATHS]`, etc.).
190
+
191
+ ## Tinkerers
192
+
193
+ If you self-compile MacroQuest or use a discord friend's copy, you can still keep your scripts and plugins in sync with redfetch by opting out of Very Vanilla:
194
+
195
+ ```powershell
196
+ redfetch.exe config SPECIAL_RESOURCES.1974.opt_in false --server LIVE
197
+ redfetch.exe config SPECIAL_RESOURCES.60.opt_in false --server EMU
198
+ redfetch.exe config SPECIAL_RESOURCES.2218.opt_in false --server TEST
199
+ ```
200
+
201
+ Then assign the *Very Vanilla MQ* path to your self-compiled MacroQuest.
202
+
203
+ ### Custom post-update launch
204
+
205
+ redfetch can launch extra programs after an update completes. Aside from the normal UI toggles, you can add `custom` to `POST_UPDATE_LAUNCH.targets` in `settings.local.toml`, then set `command` to whatever redfetch should run:
206
+
207
+ ```toml
208
+ [LIVE.POST_UPDATE_LAUNCH]
209
+ targets = ["custom"]
210
+ command = ['C:\Tools\AfterRedfetch\after-update.exe', '--server', 'LIVE']
211
+ ```
212
+
213
+ You can also combine it with the built-in post-update launches. For example, to start EQBCS and MySEQ and run your own Python script:
214
+
215
+ ```toml
216
+ [LIVE.POST_UPDATE_LAUNCH]
217
+ targets = ["myseq", "custom", "eqbcs"]
218
+ command = ["C:\\Users\\Public\\Python\\python.exe", "C:\\Users\\Public\\redfetch\\after_update.py"]
219
+ ```
220
+
221
+ You can set these per-server, e.g. `[TEST.POST_UPDATE_LAUNCH]`, or global `[DEFAULT.POST_UPDATE_LAUNCH]`.
222
+
223
+
224
+ ## Trailmap
225
+ - Add custom buttons for "fetch" tab.
226
+ - Option: Close after update
227
+ - Indicate when updated VV is available
228
+ - Run from MQ
229
+ - Deeper integration with the forums
230
+
231
+ ![Watchers on RedGuides](https://www.redguides.com/community/resources/redfetch.3177/watchers-sparkline?months=12&w=500&h=180)
232
+
233
+ ## Contributing
234
+
235
+ I'd love help, conceptually and technically. I'm not a developer and this is my first big python script.
236
+
237
+ > [!NOTE]
238
+ > This project is built with LLM assistance.
239
+
240
+ To set up a [development environment](https://hatch.pypa.io/latest/environment/),
241
+
242
+ ```bash
243
+ git clone https://github.com/RedGuides/redfetch
244
+ cd redfetch
245
+ pip install hatch
246
+ hatch env create dev
247
+ hatch shell dev
248
+ ```
249
+ You can then run your dev version with,
250
+
251
+ `redfetch`
252
+
253
+ Or if the issue is ui-specific, run the [terminal UI in debug mode](https://textual.textualize.io/guide/devtools/#live-editing),
254
+
255
+ `textual run --dev .\src\redfetch\main.py`
256
+
257
+ When you're done, type `exit` to leave the shell.
@@ -0,0 +1,37 @@
1
+ redfetch/__about__.py,sha256=kjMXs_RelAfH3nsfXcggy4eqwR8yDdd160wJ9EHB63g,520
2
+ redfetch/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ redfetch/api.py,sha256=bsjEQ34qV2cEPG4PyCbIs-UFg5RTif7BgW9gLLmCWVY,4795
4
+ redfetch/auth.py,sha256=vmwCnRQlmnUgDwYseu_EHZ4KKKfC-aG4VeIHE9ZNDyI,20005
5
+ redfetch/config.py,sha256=4rJJk8Rs9t4UyFkw_rP6qs83UDwA9hjNmxya9veXjoU,16265
6
+ redfetch/config_firstrun.py,sha256=MgXNPGE3EbLFH5wWdxOxVOThi3YQB2btB6UQeibWPNg,21196
7
+ redfetch/desktop_shortcut.py,sha256=yE2I6fHLGYjUQyHFzZA6Xr2LLSGvFJTgdB4PqWIH3Q4,2345
8
+ redfetch/detecteq.py,sha256=99r7jowZOT59BFmFJDQ1OsiXqnaeqLSpivuI14kHf9E,3777
9
+ redfetch/download.py,sha256=fQRTtkN-PXz9qVH8tZrkuz0ehg3B5g3Ux5eLv1S9aIs,11344
10
+ redfetch/listener.py,sha256=JC1ZOy1lGrJAKgOs7JVeMOulXu-c0q6tsODjN6bUWug,7756
11
+ redfetch/main.py,sha256=0ft3_BJvU6qA2cjt5e3wp_fkL7kzo_hnESVizXZxTEI,27118
12
+ redfetch/meta.py,sha256=8-AtUmHhktNWKO91g109G2RBdeJGw-WTr3Y98cxrmRA,20309
13
+ redfetch/navmesh.py,sha256=Yg4SdQFwarTxuyECH-5DCG8m3VZeRqE5v9fh95HUhW8,13193
14
+ redfetch/net.py,sha256=9FJegQ4FjgvlgQi2M6Cgcv4iRoFqjHdONENzfRTDzdk,3801
15
+ redfetch/processes.py,sha256=Cv0myebAy2KwcnJTdBNmz2vEO_eP9jJ6wAl82brjsSw,3984
16
+ redfetch/push.py,sha256=Je_I_XcuBDl6KzHuo97AI1_FQB-CUf3EoOVzbZPrABk,9645
17
+ redfetch/runtime_errors.py,sha256=U5AhI3Jwl5t7D0sLoKCbVWCx4BIBKSd1ZbzdxAYMddY,2848
18
+ redfetch/settings.toml,sha256=JGm8ZU30maPc-HVYxbAd154xUnwJinQu5Bj4NrywDtA,5910
19
+ redfetch/special.py,sha256=iaFJ7FcTp58-HIfxmmdZjLdnvpu2diEMtkxJiY_8E6g,3087
20
+ redfetch/store.py,sha256=HmkmV2u557vtlLZuOLO89d0uco6lT5SLuYBH1Iy2Xk4,16054
21
+ redfetch/sync.py,sha256=vnd2V8XP6OoOxfxj-eMf8fca53lqadF4Ro_vmbs0sMo,8371
22
+ redfetch/sync_discovery.py,sha256=XvwVQpG8nWMhS_-d42EU5NDPP8pGwchWGIPuV7Rppks,13396
23
+ redfetch/sync_executor.py,sha256=gcxA-UD6Spb7TVo1KORlPbW5eS9-x9r8hKDzSanlcGs,5382
24
+ redfetch/sync_planner.py,sha256=xNZdufziwTk_N-u0VMZ1lA40GdZfvtcOcKX1EkyHvf4,7216
25
+ redfetch/sync_remote.py,sha256=pD0sQl5aiE10gwSBH7o2rkibtiUK5OOZ5V3f0yYR6cw,6875
26
+ redfetch/sync_types.py,sha256=fPCVr6ZPbgChVpgHJa-4L4s7-_DcyUvZlJN4Ze33ur4,12058
27
+ redfetch/terminal_ui.py,sha256=yh6sF_ropqYfDZs2Jhnvl04rPwUx20pXO874tIyr2HY,95146
28
+ redfetch/terminal_ui.tcss,sha256=Wc72DNYgLq5zO4QP72vIPaB7J3XRWf-qDlFDLRwj9-A,10974
29
+ redfetch/unloadmq.py,sha256=SG-Zgd7_rKbx5Scyqr4qcdcRWe_4oodJiLb0aVPn-uM,5356
30
+ redfetch/update_status.py,sha256=uqfut6U5k_ZVn9j0NV1fdG5h8q39bVgnTEwOMBB3NNw,1874
31
+ redfetch/utils.py,sha256=TFbazOiXYwRk6NW82BI72w2_z-nB9EH_tCHtMPnggrA,8457
32
+ redfetch/redfetch.ico,sha256=iMUv7RKxph5H9L9w3nll8aTA3DOSgu_OU_irW2ixtgI,32038
33
+ redfetch-1.3.0.dist-info/METADATA,sha256=ix16apnjvkRaz9KTm9IfMIsOWIh_g7VAqSgf-X97-zQ,11444
34
+ redfetch-1.3.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
35
+ redfetch-1.3.0.dist-info/entry_points.txt,sha256=c0H5w7j7HZcWLAzdmJzAFVY7XGqT7kEvQNRLdbFGeJ0,48
36
+ redfetch-1.3.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
37
+ redfetch-1.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.30.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ redfetch = redfetch.main:main