redfetch 1.1.2__tar.gz → 1.3.0__tar.gz

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.
Files changed (63) hide show
  1. {redfetch-1.1.2 → redfetch-1.3.0}/.github/workflows/pipeline.yml +57 -14
  2. {redfetch-1.1.2 → redfetch-1.3.0}/CHANGELOG.md +26 -0
  3. {redfetch-1.1.2 → redfetch-1.3.0}/PKG-INFO +50 -15
  4. {redfetch-1.1.2 → redfetch-1.3.0}/README.md +35 -2
  5. {redfetch-1.1.2 → redfetch-1.3.0}/pyproject.toml +13 -11
  6. redfetch-1.3.0/src/redfetch/__about__.py +24 -0
  7. redfetch-1.3.0/src/redfetch/api.py +134 -0
  8. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/auth.py +235 -64
  9. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/config.py +231 -82
  10. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/config_firstrun.py +26 -12
  11. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/desktop_shortcut.py +0 -7
  12. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/download.py +74 -89
  13. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/listener.py +18 -29
  14. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/main.py +150 -57
  15. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/meta.py +14 -11
  16. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/navmesh.py +2 -3
  17. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/net.py +5 -5
  18. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/processes.py +29 -15
  19. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/push.py +26 -31
  20. redfetch-1.3.0/src/redfetch/runtime_errors.py +96 -0
  21. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/settings.toml +15 -5
  22. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/special.py +9 -29
  23. redfetch-1.3.0/src/redfetch/store.py +505 -0
  24. redfetch-1.3.0/src/redfetch/sync.py +261 -0
  25. redfetch-1.3.0/src/redfetch/sync_discovery.py +352 -0
  26. redfetch-1.3.0/src/redfetch/sync_executor.py +164 -0
  27. redfetch-1.3.0/src/redfetch/sync_planner.py +196 -0
  28. redfetch-1.3.0/src/redfetch/sync_remote.py +182 -0
  29. redfetch-1.3.0/src/redfetch/sync_types.py +348 -0
  30. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/terminal_ui.py +231 -160
  31. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/terminal_ui.tcss +47 -14
  32. redfetch-1.3.0/src/redfetch/update_status.py +62 -0
  33. redfetch-1.3.0/src/redfetch/utils.py +256 -0
  34. redfetch-1.3.0/tests/test_breadcrumb.py +56 -0
  35. redfetch-1.3.0/tests/test_cancellation_e2e.py +123 -0
  36. redfetch-1.3.0/tests/test_check.py +144 -0
  37. redfetch-1.3.0/tests/test_env_selection.py +164 -0
  38. {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_first_run_setup.py +20 -39
  39. redfetch-1.3.0/tests/test_licensed_resources_filtering.py +320 -0
  40. redfetch-1.3.0/tests/test_path_reset.py +187 -0
  41. {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_publish_message_file_fallback.py +2 -0
  42. redfetch-1.3.0/tests/test_settings_local_deltas.py +56 -0
  43. redfetch-1.3.0/tests/test_sync_pipeline_regressions.py +530 -0
  44. redfetch-1.3.0/tests/test_sync_queue_rules.py +344 -0
  45. redfetch-1.1.2/src/redfetch/ResourceInstallBtn.js +0 -237
  46. redfetch-1.1.2/src/redfetch/__about__.py +0 -34
  47. redfetch-1.1.2/src/redfetch/api.py +0 -354
  48. redfetch-1.1.2/src/redfetch/models.py +0 -47
  49. redfetch-1.1.2/src/redfetch/store.py +0 -597
  50. redfetch-1.1.2/src/redfetch/sync.py +0 -447
  51. redfetch-1.1.2/src/redfetch/utils.py +0 -236
  52. redfetch-1.1.2/tests/test_cancellation_e2e.py +0 -172
  53. redfetch-1.1.2/tests/test_licensed_resources_filtering.py +0 -87
  54. redfetch-1.1.2/tests/test_path_reset.py +0 -138
  55. redfetch-1.1.2/tests/test_special_resources_e2e.py +0 -485
  56. {redfetch-1.1.2 → redfetch-1.3.0}/.gitignore +0 -0
  57. {redfetch-1.1.2 → redfetch-1.3.0}/LICENSE +0 -0
  58. {redfetch-1.1.2 → redfetch-1.3.0}/redfetch.ico +0 -0
  59. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/__init__.py +0 -0
  60. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/detecteq.py +0 -0
  61. {redfetch-1.1.2 → redfetch-1.3.0}/src/redfetch/unloadmq.py +0 -0
  62. {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_is_special_or_dependency.py +0 -0
  63. {redfetch-1.1.2 → redfetch-1.3.0}/tests/test_uninstall_e2e.py +0 -0
@@ -27,13 +27,13 @@ jobs:
27
27
  package-version: ${{ steps.get_version.outputs.version }}
28
28
  steps:
29
29
  - name: Checkout Code
30
- uses: actions/checkout@v4
30
+ uses: actions/checkout@v6
31
31
  with:
32
32
  fetch-depth: 0 # Ensure full Git history is available for versioning
33
33
  fetch-tags: true
34
34
 
35
35
  - name: Set Up Python
36
- uses: actions/setup-python@v5
36
+ uses: actions/setup-python@v6
37
37
  with:
38
38
  python-version: '3.x'
39
39
 
@@ -85,7 +85,7 @@ jobs:
85
85
  project-version: ${{ steps.set_version.outputs.version }}
86
86
  steps:
87
87
  - name: Checkout Code
88
- uses: actions/checkout@v4
88
+ uses: actions/checkout@v6
89
89
  with:
90
90
  fetch-depth: 0 # Ensure full Git history is available for versioning
91
91
 
@@ -110,7 +110,7 @@ jobs:
110
110
  Get-Content $env:GITHUB_ENV
111
111
 
112
112
  - name: Set Up Python
113
- uses: actions/setup-python@v5
113
+ uses: actions/setup-python@v6
114
114
  with:
115
115
  python-version: '3.x'
116
116
 
@@ -184,17 +184,60 @@ jobs:
184
184
  --set-version-string "InternalName" "redfetch" `
185
185
  --set-icon "redfetch.ico"
186
186
 
187
+ - name: Install Google Cloud KMS CNG Provider
188
+ shell: pwsh
189
+ run: |
190
+ if (Test-Path "$env:SystemRoot\System32\kmscng.dll") {
191
+ Write-Host "kmscng already installed."
192
+ return
193
+ }
194
+ $url = "https://github.com/GoogleCloudPlatform/kms-integrations/releases/download/cng-v1.3/kmscng-1.3-windows-amd64.zip"
195
+ $zip = Join-Path $env:RUNNER_TEMP "kmscng.zip"
196
+ $dest = Join-Path $env:RUNNER_TEMP "kmscng"
197
+ Invoke-WebRequest $url -OutFile $zip
198
+ Expand-Archive -Path $zip -DestinationPath $dest -Force
199
+ $msi = Get-ChildItem -Path $dest -Recurse -Filter *.msi | Select-Object -First 1
200
+ if (-not $msi) { throw "kmscng MSI not found in archive" }
201
+ Start-Process msiexec.exe -ArgumentList "/i `"$($msi.FullName)`" /qn /norestart" -Wait -NoNewWindow
202
+ if (-not (Test-Path "$env:SystemRoot\System32\kmscng.dll")) { throw "kmscng installation failed" }
203
+ Write-Host "kmscng installed."
204
+
187
205
  - name: Sign Executable
188
- uses: dlemstra/code-sign-action@v1
189
- with:
190
- certificate: '${{ secrets.CERTIFICATE_PFX }}'
191
- password: '${{ secrets.CERTIFICATE_PFX_PASSWORD }}'
192
- files: |
193
- redfetch.exe
194
- description: 'redfetch executable'
206
+ shell: pwsh
207
+ env:
208
+ KMS_KEY: projects/code-signing-rg/locations/global/keyRings/redkeys/cryptoKeys/rgkey/cryptoKeyVersions/1
209
+ SIGNING_CERT: ${{ secrets.SIGNING_CERT }}
210
+ GCP_SA_KEY: ${{ secrets.GCP_SA_KEY }}
211
+ run: |
212
+ $CertFile = Join-Path $env:RUNNER_TEMP "user.crt"
213
+ $KeyFile = Join-Path $env:RUNNER_TEMP "gcp-sa.json"
214
+ [IO.File]::WriteAllBytes($CertFile, [Convert]::FromBase64String($env:SIGNING_CERT))
215
+ [IO.File]::WriteAllText($KeyFile, $env:GCP_SA_KEY)
216
+ $env:GOOGLE_APPLICATION_CREDENTIALS = $KeyFile
217
+
218
+ $signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits" -Recurse -Filter "signtool.exe" |
219
+ Where-Object { $_.FullName -match "\\x64\\" } |
220
+ Sort-Object LastWriteTime -Descending |
221
+ Select-Object -First 1 -ExpandProperty FullName
222
+
223
+ if (-not $signtool) { throw "signtool.exe not found" }
224
+ Write-Host "Using signtool: $signtool"
225
+
226
+ $tsaServers = @('http://timestamp.digicert.com', 'http://timestamp.sectigo.com')
227
+ $maxAttempts = 5
228
+ for ($attempt = 1; $attempt -le $maxAttempts; $attempt++) {
229
+ $tsa = $tsaServers[($attempt - 1) % $tsaServers.Count]
230
+ & $signtool sign /fd SHA256 /f $CertFile /csp "Google Cloud KMS Provider" /kc $env:KMS_KEY /tr $tsa /td sha256 /v redfetch.exe
231
+ if ($LASTEXITCODE -eq 0) { break }
232
+ if ($attempt -eq $maxAttempts) { throw "Signing failed after $maxAttempts attempts" }
233
+ Start-Sleep -Seconds 2
234
+ }
235
+ & $signtool verify /pa /v redfetch.exe
236
+
237
+ Remove-Item -Force $CertFile, $KeyFile
195
238
 
196
239
  - name: Upload Executable Artifact
197
- uses: actions/upload-artifact@v4
240
+ uses: actions/upload-artifact@v7
198
241
  with:
199
242
  name: redfetch.exe
200
243
  path: redfetch.exe
@@ -205,9 +248,9 @@ jobs:
205
248
  runs-on: ubuntu-latest
206
249
  steps:
207
250
  - name: Checkout Code
208
- uses: actions/checkout@v4
251
+ uses: actions/checkout@v6
209
252
  - name: Download Executable Artifact
210
- uses: actions/download-artifact@v4
253
+ uses: actions/download-artifact@v8
211
254
  with:
212
255
  name: redfetch.exe
213
256
  - name: Redguides Publish
@@ -5,6 +5,32 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [1.3.0] - 2026-06-26
9
+
10
+ ### Added
11
+ - `--server` on `update` and `download`: use a different server for one run.
12
+ - TUI: "Also start post-update" — launch EQBCS, MySEQ, or a custom program after a successful update. ([#18](https://github.com/RedGuides/redfetch/issues/18))
13
+ - Staff picks: Ninjadvloot.inc is now on LIVE/TEST/EMU.
14
+
15
+ ### Changed
16
+ - Config paths that contain `eqgame.exe` are allowed with a warning instead of being blocked outright. ([#22](https://github.com/RedGuides/redfetch/issues/22))
17
+ - TUI: "Close MQ pre-update" and "Start MQ post-update" are now No / Ask / Yes toggles, so you can pick the prompt-each-time ("Ask") behavior from the UI instead of only on/off.
18
+ - Only settings you've actually changed are written to `settings.local.toml`, keeping the file tidy.
19
+
20
+ ### Fixed
21
+ - Licensed resources will now download even if you have duplicates. ([#24](https://github.com/RedGuides/redfetch/issues/24))
22
+ - Update checks notice when a resource's subfolder changes.
23
+ - Fixed "RecursionError" on startup by rolling back a dependency.
24
+
25
+ ## [1.2.0] - 2026-04-05
26
+
27
+ ### Added
28
+ - Custom category directories: override where `lua`, `macros`, or `plugins` install via `CATEGORY_PATHS` in `settings.local.toml`.
29
+ - Clearer messages for users on why certain resources are skipped or blocked.
30
+
31
+ ### Changed
32
+ - Rewrote the sync pipeline with separate discovery, planning, and execution stages.
33
+
8
34
  ## [1.1.2] - 2026-02-16
9
35
 
10
36
  ### Fixed
@@ -1,13 +1,13 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redfetch
3
- Version: 1.1.2
3
+ Version: 1.3.0
4
4
  Summary: Download and publish EverQuest scripts and software using the RedGuides API
5
5
  Project-URL: Homepage, https://www.redguides.com
6
6
  Project-URL: Documentation, https://www.redguides.com/community/resources/redfetch.3177/
7
7
  Project-URL: Repository, https://github.com/RedGuides/redfetch
8
8
  Project-URL: Issues, https://github.com/RedGuides/redfetch/issues
9
9
  Project-URL: Changelog, https://github.com/RedGuides/redfetch/blob/main/CHANGELOG.md
10
- Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/ab2c7322507ec27526aed8b90ef1ca15a8bcab8f.zip
10
+ Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/aaf2d42ab82d45abeadfafa943e2b61abba2cb60.zip
11
11
  Author-email: Redbot <ask@redguides.com>
12
12
  License-Expression: GPL-3.0-or-later
13
13
  License-File: LICENSE
@@ -17,27 +17,29 @@ Classifier: License :: OSI Approved :: GNU General Public License v3 or later (G
17
17
  Classifier: Operating System :: OS Independent
18
18
  Classifier: Programming Language :: Python :: 3.12
19
19
  Classifier: Topic :: Utilities
20
+ Requires-Python: >=3.12
20
21
  Requires-Dist: aiofiles~=25.1
21
- Requires-Dist: aiohttp~=3.13
22
+ Requires-Dist: aiohttp~=3.14
22
23
  Requires-Dist: aiosqlite~=0.22
23
- Requires-Dist: cachetools~=7.0
24
+ Requires-Dist: cachetools~=7.1
24
25
  Requires-Dist: diskcache~=5.6
25
- Requires-Dist: dynaconf~=3.2
26
+ Requires-Dist: dynaconf<3.3,~=3.2
26
27
  Requires-Dist: httpx[http2]~=0.28
27
28
  Requires-Dist: keepachangelog~=2.0
28
- Requires-Dist: keyring~=25.6
29
+ Requires-Dist: keyring~=25.7
29
30
  Requires-Dist: md2bbcode
30
- Requires-Dist: packaging~=26.0
31
- Requires-Dist: platformdirs~=4.5
31
+ Requires-Dist: packaging~=26.2
32
+ Requires-Dist: platformdirs~=4.10
32
33
  Requires-Dist: psutil~=7.2
34
+ Requires-Dist: pydantic~=2.13
33
35
  Requires-Dist: pyperclip~=1.11
34
36
  Requires-Dist: pywin32; sys_platform == 'win32'
35
- Requires-Dist: rich~=14.3
37
+ Requires-Dist: rich~=15.0
36
38
  Requires-Dist: tenacity~=9.1
37
- Requires-Dist: textual-fspicker~=0.6
38
- Requires-Dist: textual~=7.5
39
- Requires-Dist: tomlkit~=0.14
40
- Requires-Dist: typer~=0.21
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
41
43
  Description-Content-Type: text/markdown
42
44
 
43
45
  ![six wizards levitating a package, the word redfetch underneath](https://www.redguides.com/images/redfetchlogo.png)
@@ -108,8 +110,10 @@ To run redfetch from the command line:
108
110
  > ### 📦 Resource Management
109
111
  > - `update` - Update all watched and special resources
110
112
  > - `--force` / `-f` - Force re-download of all watched resources
113
+ > - `--server` / `-s` - Switch to this server before updating (`LIVE`, `TEST`, `EMU`)
111
114
  > - `download <ID_OR_URL>` - Download a specific resource by ID or URL
112
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`)
113
117
  > - `list` - List resources and dependencies in the cache database
114
118
  > - `reset` - Reset download dates for watched resources
115
119
  >
@@ -172,6 +176,18 @@ If there are local files you don't want overwritten by a resource, you can add t
172
176
  navmesh = ["befallen.navmesh", "innothuleb.navmesh"]
173
177
  ```
174
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
+
175
191
  ## Tinkerers
176
192
 
177
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:
@@ -184,12 +200,31 @@ redfetch.exe config SPECIAL_RESOURCES.2218.opt_in false --server TEST
184
200
 
185
201
  Then assign the *Very Vanilla MQ* path to your self-compiled MacroQuest.
186
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
+
187
224
  ## Trailmap
188
225
  - Add custom buttons for "fetch" tab.
189
226
  - Option: Close after update
190
- - Launch programs with cli options
191
227
  - Indicate when updated VV is available
192
- - Launch more than just mq (eqbcs, etc) upon update.
193
228
  - Run from MQ
194
229
  - Deeper integration with the forums
195
230
 
@@ -66,8 +66,10 @@ To run redfetch from the command line:
66
66
  > ### 📦 Resource Management
67
67
  > - `update` - Update all watched and special resources
68
68
  > - `--force` / `-f` - Force re-download of all watched resources
69
+ > - `--server` / `-s` - Switch to this server before updating (`LIVE`, `TEST`, `EMU`)
69
70
  > - `download <ID_OR_URL>` - Download a specific resource by ID or URL
70
71
  > - `--force` / `-f` - Force re-download by resetting this resource's download date
72
+ > - `--server` / `-s` - Switch to this server before downloading (`LIVE`, `TEST`, `EMU`)
71
73
  > - `list` - List resources and dependencies in the cache database
72
74
  > - `reset` - Reset download dates for watched resources
73
75
  >
@@ -130,6 +132,18 @@ If there are local files you don't want overwritten by a resource, you can add t
130
132
  navmesh = ["befallen.navmesh", "innothuleb.navmesh"]
131
133
  ```
132
134
 
135
+ ### Custom category directories
136
+
137
+ 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`:
138
+
139
+ ```toml
140
+ [DEFAULT.CATEGORY_PATHS]
141
+ lua = 'D:\\shared\\lua'
142
+ macros = 'D:\\shared\\macros'
143
+ ```
144
+
145
+ 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.).
146
+
133
147
  ## Tinkerers
134
148
 
135
149
  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:
@@ -142,12 +156,31 @@ redfetch.exe config SPECIAL_RESOURCES.2218.opt_in false --server TEST
142
156
 
143
157
  Then assign the *Very Vanilla MQ* path to your self-compiled MacroQuest.
144
158
 
159
+ ### Custom post-update launch
160
+
161
+ 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:
162
+
163
+ ```toml
164
+ [LIVE.POST_UPDATE_LAUNCH]
165
+ targets = ["custom"]
166
+ command = ['C:\Tools\AfterRedfetch\after-update.exe', '--server', 'LIVE']
167
+ ```
168
+
169
+ 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:
170
+
171
+ ```toml
172
+ [LIVE.POST_UPDATE_LAUNCH]
173
+ targets = ["myseq", "custom", "eqbcs"]
174
+ command = ["C:\\Users\\Public\\Python\\python.exe", "C:\\Users\\Public\\redfetch\\after_update.py"]
175
+ ```
176
+
177
+ You can set these per-server, e.g. `[TEST.POST_UPDATE_LAUNCH]`, or global `[DEFAULT.POST_UPDATE_LAUNCH]`.
178
+
179
+
145
180
  ## Trailmap
146
181
  - Add custom buttons for "fetch" tab.
147
182
  - Option: Close after update
148
- - Launch programs with cli options
149
183
  - Indicate when updated VV is available
150
- - Launch more than just mq (eqbcs, etc) upon update.
151
184
  - Run from MQ
152
185
  - Deeper integration with the forums
153
186
 
@@ -3,6 +3,7 @@ name = "redfetch"
3
3
  description = "Download and publish EverQuest scripts and software using the RedGuides API"
4
4
  readme = "README.md"
5
5
  license = "GPL-3.0-or-later"
6
+ requires-python = ">=3.12"
6
7
  dynamic = ["version", "urls"]
7
8
  authors = [
8
9
  { name = "Redbot", email = "ask@redguides.com" }
@@ -19,24 +20,25 @@ classifiers = [
19
20
  dependencies = [
20
21
  "aiofiles~=25.1",
21
22
  "aiosqlite~=0.22",
22
- "aiohttp~=3.13",
23
- "cachetools~=7.0",
23
+ "aiohttp~=3.14",
24
+ "cachetools~=7.1",
24
25
  "diskcache~=5.6",
25
- "dynaconf~=3.2",
26
+ "dynaconf~=3.2,<3.3", # 3.3 has a bug
26
27
  "httpx[http2]~=0.28",
27
28
  "keepachangelog~=2.0",
28
- "keyring~=25.6",
29
+ "keyring~=25.7",
29
30
  "md2bbcode",
30
- "packaging~=26.0",
31
- "platformdirs~=4.5",
31
+ "packaging~=26.2",
32
+ "pydantic~=2.13",
33
+ "platformdirs~=4.10",
32
34
  "psutil~=7.2",
33
35
  "pyperclip~=1.11",
34
- "rich~=14.3",
36
+ "rich~=15.0",
35
37
  "tenacity~=9.1",
36
- "textual~=7.5",
37
- "textual-fspicker~=0.6",
38
- "tomlkit~=0.14",
39
- "typer~=0.21",
38
+ "textual~=8.2",
39
+ "textual-fspicker~=1.0",
40
+ "tomlkit~=0.15",
41
+ "typer~=0.26",
40
42
  "pywin32; sys_platform == 'win32'"
41
43
  ]
42
44
 
@@ -0,0 +1,24 @@
1
+ # file generated by vcs-versioning
2
+ # don't change, don't track in version control
3
+ from __future__ import annotations
4
+
5
+ __all__ = [
6
+ "__version__",
7
+ "__version_tuple__",
8
+ "version",
9
+ "version_tuple",
10
+ "__commit_id__",
11
+ "commit_id",
12
+ ]
13
+
14
+ version: str
15
+ __version__: str
16
+ __version_tuple__: tuple[int | str, ...]
17
+ version_tuple: tuple[int | str, ...]
18
+ commit_id: str | None
19
+ __commit_id__: str | None
20
+
21
+ __version__ = version = '1.3.0'
22
+ __version_tuple__ = version_tuple = (1, 3, 0)
23
+
24
+ __commit_id__ = commit_id = None
@@ -0,0 +1,134 @@
1
+ """Resource API client: fetch_*() takes a client, get_*() is self-contained."""
2
+
3
+ import asyncio
4
+
5
+ import httpx
6
+ from redfetch import auth
7
+ from redfetch import net
8
+ from redfetch.sync_types import RemoteStatus, SyncModel
9
+
10
+ BASE_URL = net.BASE_URL
11
+
12
+
13
+ class ResourceRecord(SyncModel):
14
+ """Result of a single-resource API check: status plus optional payload or error."""
15
+ resource_id: str
16
+ status: RemoteStatus
17
+ resource: dict | None = None
18
+ error: str | None = None
19
+
20
+
21
+ async def fetch_watched_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
22
+ """Fetch a single page of watched resources."""
23
+ url = f'{BASE_URL}/api/rgwatched'
24
+ try:
25
+ data = await net.get_json(client, url, params={'page': page})
26
+ last_page = data['pagination']['last_page']
27
+ items = data.get('resources', [])
28
+ return items, last_page
29
+ except Exception as e:
30
+ print(f"Error fetching watched resources page {page}: {e}")
31
+ return [], 0
32
+
33
+
34
+ async def fetch_licenses_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
35
+ """Fetch a single page of user licenses."""
36
+ url = f'{BASE_URL}/api/user-licenses'
37
+ try:
38
+ data = await net.get_json(client, url, params={'page': page})
39
+ last_page = data['pagination']['last_page']
40
+ items = data.get('licenses', [])
41
+ return items, last_page
42
+ except Exception as e:
43
+ print(f"Error fetching licenses page {page}: {e}")
44
+ return [], 0
45
+
46
+
47
+ async def fetch_resource_record(client: httpx.AsyncClient, resource_id: str) -> ResourceRecord:
48
+ """Fetch a single resource and classify its availability."""
49
+ url = f'{BASE_URL}/api/resources/{resource_id}'
50
+ try:
51
+ data = await net.get_json(client, url)
52
+ resource = data['resource']
53
+ current_files = resource.get('current_files') or []
54
+ if not resource.get('can_download', False):
55
+ status: RemoteStatus = 'access_denied'
56
+ elif len(current_files) == 0:
57
+ status = 'no_files'
58
+ elif len(current_files) > 1:
59
+ status = 'multiple_files'
60
+ else:
61
+ status = 'downloadable'
62
+ return ResourceRecord(resource_id=resource_id, status=status, resource=resource)
63
+ except httpx.HTTPStatusError as e:
64
+ status = 'not_found' if e.response.status_code == 404 else 'fetch_error'
65
+ return ResourceRecord(resource_id=resource_id, status=status, error=str(e))
66
+ except Exception as e:
67
+ return ResourceRecord(resource_id=resource_id, status='fetch_error', error=str(e))
68
+
69
+
70
+ async def fetch_resource_records_batch(client: httpx.AsyncClient, resource_ids: list[str]) -> list[ResourceRecord]:
71
+ """Fetch multiple resource records concurrently."""
72
+ if not resource_ids:
73
+ return []
74
+ return list(await asyncio.gather(
75
+ *(fetch_resource_record(client, rid) for rid in resource_ids)
76
+ ))
77
+
78
+
79
+ async def get_resource_details(resource_id: int, headers: dict) -> dict:
80
+ """Retrieve details of a specific resource from the API."""
81
+ url = f'{BASE_URL}/api/resources/{resource_id}'
82
+ async with httpx.AsyncClient(headers=headers, http2=True, timeout=30.0) as client:
83
+ response = await client.get(url)
84
+ response.raise_for_status()
85
+ return response.json()['resource']
86
+
87
+
88
+ async def fetch_watched_resources(client: httpx.AsyncClient) -> list:
89
+ """Fetch all watched resources with concurrent pagination."""
90
+ items, total_pages = await fetch_watched_page(client, 1)
91
+ if total_pages <= 1:
92
+ return items
93
+
94
+ coros = [fetch_watched_page(client, p) for p in range(2, total_pages + 1)]
95
+ page_results = await asyncio.gather(*coros)
96
+
97
+ for page_items, _ in page_results:
98
+ items.extend(page_items)
99
+
100
+ return items
101
+
102
+
103
+ async def fetch_licenses(client: httpx.AsyncClient) -> list:
104
+ """Fetch all user licenses with concurrent pagination."""
105
+ items, total_pages = await fetch_licenses_page(client, 1)
106
+ if total_pages <= 1:
107
+ return items
108
+ coros = [fetch_licenses_page(client, p) for p in range(2, total_pages + 1)]
109
+ page_results = await asyncio.gather(*coros)
110
+
111
+ for page_items, _ in page_results:
112
+ items.extend(page_items)
113
+
114
+ return items
115
+
116
+
117
+ _KISS_CACHE_TTL_SECONDS = 60
118
+
119
+
120
+ async def is_kiss_downloadable(headers, force_refresh: bool = False):
121
+ """Check for level 2 access by checking KISS."""
122
+ cache = auth.get_disk_cache()
123
+ cache_key = "kiss"
124
+
125
+ if not force_refresh:
126
+ cached = cache.get(cache_key)
127
+ if cached is not None:
128
+ return bool(cached)
129
+
130
+ async with httpx.AsyncClient(headers=headers, http2=True) as client:
131
+ record = await fetch_resource_record(client, "4")
132
+ result = record.status == "downloadable"
133
+ cache.set(cache_key, bool(result), expire=_KISS_CACHE_TTL_SECONDS)
134
+ return result