redfetch 1.1.2__tar.gz → 1.2.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.
- {redfetch-1.1.2 → redfetch-1.2.0}/.github/workflows/pipeline.yml +24 -14
- {redfetch-1.1.2 → redfetch-1.2.0}/CHANGELOG.md +9 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/PKG-INFO +17 -4
- {redfetch-1.1.2 → redfetch-1.2.0}/README.md +12 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/pyproject.toml +3 -2
- redfetch-1.2.0/src/redfetch/__about__.py +24 -0
- redfetch-1.2.0/src/redfetch/api.py +135 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/auth.py +235 -64
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/config.py +31 -8
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/config_firstrun.py +2 -3
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/download.py +74 -89
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/listener.py +16 -29
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/main.py +48 -55
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/meta.py +10 -11
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/navmesh.py +2 -3
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/net.py +5 -5
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/processes.py +2 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/push.py +24 -31
- redfetch-1.2.0/src/redfetch/runtime_errors.py +96 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/special.py +7 -29
- redfetch-1.2.0/src/redfetch/store.py +505 -0
- redfetch-1.2.0/src/redfetch/sync.py +240 -0
- redfetch-1.2.0/src/redfetch/sync_discovery.py +339 -0
- redfetch-1.2.0/src/redfetch/sync_executor.py +162 -0
- redfetch-1.2.0/src/redfetch/sync_planner.py +194 -0
- redfetch-1.2.0/src/redfetch/sync_remote.py +179 -0
- redfetch-1.2.0/src/redfetch/sync_types.py +337 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/terminal_ui.py +61 -59
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/terminal_ui.tcss +34 -3
- redfetch-1.2.0/src/redfetch/utils.py +149 -0
- redfetch-1.2.0/tests/test_cancellation_e2e.py +123 -0
- redfetch-1.2.0/tests/test_licensed_resources_filtering.py +238 -0
- redfetch-1.2.0/tests/test_path_reset.py +187 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/tests/test_publish_message_file_fallback.py +2 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/tests/test_special_resources_e2e.py +36 -15
- redfetch-1.2.0/tests/test_sync_pipeline_regressions.py +465 -0
- redfetch-1.2.0/tests/test_sync_queue_rules.py +344 -0
- redfetch-1.1.2/src/redfetch/__about__.py +0 -34
- redfetch-1.1.2/src/redfetch/api.py +0 -354
- redfetch-1.1.2/src/redfetch/models.py +0 -47
- redfetch-1.1.2/src/redfetch/store.py +0 -597
- redfetch-1.1.2/src/redfetch/sync.py +0 -447
- redfetch-1.1.2/src/redfetch/utils.py +0 -236
- redfetch-1.1.2/tests/test_cancellation_e2e.py +0 -172
- redfetch-1.1.2/tests/test_licensed_resources_filtering.py +0 -87
- redfetch-1.1.2/tests/test_path_reset.py +0 -138
- {redfetch-1.1.2 → redfetch-1.2.0}/.gitignore +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/LICENSE +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/redfetch.ico +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/ResourceInstallBtn.js +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/__init__.py +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/desktop_shortcut.py +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/detecteq.py +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/settings.toml +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/src/redfetch/unloadmq.py +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/tests/test_first_run_setup.py +0 -0
- {redfetch-1.1.2 → redfetch-1.2.0}/tests/test_is_special_or_dependency.py +0 -0
- {redfetch-1.1.2 → redfetch-1.2.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@
|
|
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@
|
|
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@
|
|
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@
|
|
113
|
+
uses: actions/setup-python@v6
|
|
114
114
|
with:
|
|
115
115
|
python-version: '3.x'
|
|
116
116
|
|
|
@@ -185,16 +185,26 @@ jobs:
|
|
|
185
185
|
--set-icon "redfetch.ico"
|
|
186
186
|
|
|
187
187
|
- name: Sign Executable
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
188
|
+
shell: pwsh
|
|
189
|
+
run: |
|
|
190
|
+
$certPath = Join-Path $env:RUNNER_TEMP "cert.pfx"
|
|
191
|
+
[IO.File]::WriteAllBytes($certPath, [Convert]::FromBase64String("${{ secrets.CERTIFICATE_PFX }}"))
|
|
192
|
+
|
|
193
|
+
$signtool = Get-ChildItem -Path "${env:ProgramFiles(x86)}\Windows Kits" -Recurse -Filter "signtool.exe" |
|
|
194
|
+
Where-Object { $_.FullName -match "\\x64\\" } |
|
|
195
|
+
Sort-Object LastWriteTime -Descending |
|
|
196
|
+
Select-Object -First 1 -ExpandProperty FullName
|
|
197
|
+
|
|
198
|
+
if (-not $signtool) { throw "signtool.exe not found" }
|
|
199
|
+
Write-Host "Using signtool: $signtool"
|
|
200
|
+
|
|
201
|
+
& $signtool sign /f $certPath /p "${{ secrets.CERTIFICATE_PFX_PASSWORD }}" /fd sha256 /tr "http://timestamp.digicert.com" /td sha256 /v redfetch.exe
|
|
202
|
+
& $signtool verify /pa /v redfetch.exe
|
|
203
|
+
|
|
204
|
+
Remove-Item $certPath
|
|
195
205
|
|
|
196
206
|
- name: Upload Executable Artifact
|
|
197
|
-
uses: actions/upload-artifact@
|
|
207
|
+
uses: actions/upload-artifact@v7
|
|
198
208
|
with:
|
|
199
209
|
name: redfetch.exe
|
|
200
210
|
path: redfetch.exe
|
|
@@ -205,9 +215,9 @@ jobs:
|
|
|
205
215
|
runs-on: ubuntu-latest
|
|
206
216
|
steps:
|
|
207
217
|
- name: Checkout Code
|
|
208
|
-
uses: actions/checkout@
|
|
218
|
+
uses: actions/checkout@v6
|
|
209
219
|
- name: Download Executable Artifact
|
|
210
|
-
uses: actions/download-artifact@
|
|
220
|
+
uses: actions/download-artifact@v8
|
|
211
221
|
with:
|
|
212
222
|
name: redfetch.exe
|
|
213
223
|
- name: Redguides Publish
|
|
@@ -5,6 +5,15 @@ 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.2.0] - 2026-04-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- Custom category directories: override where `lua`, `macros`, or `plugins` install via `CATEGORY_PATHS` in `settings.local.toml`.
|
|
12
|
+
- Clearer messages for users on why certain resources are skipped or blocked.
|
|
13
|
+
|
|
14
|
+
### Changed
|
|
15
|
+
- Rewrote the sync pipeline with separate discovery, planning, and execution stages.
|
|
16
|
+
|
|
8
17
|
## [1.1.2] - 2026-02-16
|
|
9
18
|
|
|
10
19
|
### Fixed
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: redfetch
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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/
|
|
10
|
+
Project-URL: Source_archive, https://github.com/RedGuides/redfetch/archive/35e27c4778b6d07b5be189be22877ad5bb541d27.zip
|
|
11
11
|
Author-email: Redbot <ask@redguides.com>
|
|
12
12
|
License-Expression: GPL-3.0-or-later
|
|
13
13
|
License-File: LICENSE
|
|
@@ -30,12 +30,13 @@ Requires-Dist: md2bbcode
|
|
|
30
30
|
Requires-Dist: packaging~=26.0
|
|
31
31
|
Requires-Dist: platformdirs~=4.5
|
|
32
32
|
Requires-Dist: psutil~=7.2
|
|
33
|
+
Requires-Dist: pydantic~=2.12
|
|
33
34
|
Requires-Dist: pyperclip~=1.11
|
|
34
35
|
Requires-Dist: pywin32; sys_platform == 'win32'
|
|
35
36
|
Requires-Dist: rich~=14.3
|
|
36
37
|
Requires-Dist: tenacity~=9.1
|
|
37
|
-
Requires-Dist: textual-fspicker~=0
|
|
38
|
-
Requires-Dist: textual~=
|
|
38
|
+
Requires-Dist: textual-fspicker~=1.0
|
|
39
|
+
Requires-Dist: textual~=8.1
|
|
39
40
|
Requires-Dist: tomlkit~=0.14
|
|
40
41
|
Requires-Dist: typer~=0.21
|
|
41
42
|
Description-Content-Type: text/markdown
|
|
@@ -172,6 +173,18 @@ If there are local files you don't want overwritten by a resource, you can add t
|
|
|
172
173
|
navmesh = ["befallen.navmesh", "innothuleb.navmesh"]
|
|
173
174
|
```
|
|
174
175
|
|
|
176
|
+
### Custom category directories
|
|
177
|
+
|
|
178
|
+
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`:
|
|
179
|
+
|
|
180
|
+
```toml
|
|
181
|
+
[DEFAULT.CATEGORY_PATHS]
|
|
182
|
+
lua = 'D:\\shared\\lua'
|
|
183
|
+
macros = 'D:\\shared\\macros'
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
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.).
|
|
187
|
+
|
|
175
188
|
## Tinkerers
|
|
176
189
|
|
|
177
190
|
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:
|
|
@@ -130,6 +130,18 @@ If there are local files you don't want overwritten by a resource, you can add t
|
|
|
130
130
|
navmesh = ["befallen.navmesh", "innothuleb.navmesh"]
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
### Custom category directories
|
|
134
|
+
|
|
135
|
+
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`:
|
|
136
|
+
|
|
137
|
+
```toml
|
|
138
|
+
[DEFAULT.CATEGORY_PATHS]
|
|
139
|
+
lua = 'D:\\shared\\lua'
|
|
140
|
+
macros = 'D:\\shared\\macros'
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
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.).
|
|
144
|
+
|
|
133
145
|
## Tinkerers
|
|
134
146
|
|
|
135
147
|
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:
|
|
@@ -28,13 +28,14 @@ dependencies = [
|
|
|
28
28
|
"keyring~=25.6",
|
|
29
29
|
"md2bbcode",
|
|
30
30
|
"packaging~=26.0",
|
|
31
|
+
"pydantic~=2.12",
|
|
31
32
|
"platformdirs~=4.5",
|
|
32
33
|
"psutil~=7.2",
|
|
33
34
|
"pyperclip~=1.11",
|
|
34
35
|
"rich~=14.3",
|
|
35
36
|
"tenacity~=9.1",
|
|
36
|
-
"textual~=
|
|
37
|
-
"textual-fspicker~=0
|
|
37
|
+
"textual~=8.1",
|
|
38
|
+
"textual-fspicker~=1.0",
|
|
38
39
|
"tomlkit~=0.14",
|
|
39
40
|
"typer~=0.21",
|
|
40
41
|
"pywin32; sys_platform == 'win32'"
|
|
@@ -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.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
23
|
+
|
|
24
|
+
__commit_id__ = commit_id = None
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""Resource API client: fetch_*() takes a client, get_*() is self-contained."""
|
|
2
|
+
import asyncio
|
|
3
|
+
|
|
4
|
+
import httpx
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
from redfetch import auth
|
|
8
|
+
from redfetch import net
|
|
9
|
+
from redfetch.sync_types import RemoteStatus
|
|
10
|
+
|
|
11
|
+
BASE_URL = net.BASE_URL
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ResourceRecord(BaseModel):
|
|
15
|
+
"""Result of a single-resource API check: status plus optional payload or error."""
|
|
16
|
+
resource_id: str
|
|
17
|
+
status: RemoteStatus
|
|
18
|
+
resource: dict | None = None
|
|
19
|
+
error: str | None = None
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
async def fetch_watched_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
|
|
23
|
+
"""Fetch a single page of watched resources."""
|
|
24
|
+
url = f'{BASE_URL}/api/rgwatched'
|
|
25
|
+
try:
|
|
26
|
+
data = await net.get_json(client, url, params={'page': page})
|
|
27
|
+
last_page = data['pagination']['last_page']
|
|
28
|
+
items = data.get('resources', [])
|
|
29
|
+
return items, last_page
|
|
30
|
+
except Exception as e:
|
|
31
|
+
print(f"Error fetching watched resources page {page}: {e}")
|
|
32
|
+
return [], 0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
async def fetch_licenses_page(client: httpx.AsyncClient, page: int) -> tuple[list, int]:
|
|
36
|
+
"""Fetch a single page of user licenses."""
|
|
37
|
+
url = f'{BASE_URL}/api/user-licenses'
|
|
38
|
+
try:
|
|
39
|
+
data = await net.get_json(client, url, params={'page': page})
|
|
40
|
+
last_page = data['pagination']['last_page']
|
|
41
|
+
items = data.get('licenses', [])
|
|
42
|
+
return items, last_page
|
|
43
|
+
except Exception as e:
|
|
44
|
+
print(f"Error fetching licenses page {page}: {e}")
|
|
45
|
+
return [], 0
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def fetch_resource_record(client: httpx.AsyncClient, resource_id: str) -> ResourceRecord:
|
|
49
|
+
"""Fetch a single resource and classify its availability."""
|
|
50
|
+
url = f'{BASE_URL}/api/resources/{resource_id}'
|
|
51
|
+
try:
|
|
52
|
+
data = await net.get_json(client, url)
|
|
53
|
+
resource = data['resource']
|
|
54
|
+
current_files = resource.get('current_files') or []
|
|
55
|
+
if not resource.get('can_download', False):
|
|
56
|
+
status: RemoteStatus = 'access_denied'
|
|
57
|
+
elif len(current_files) == 0:
|
|
58
|
+
status = 'no_files'
|
|
59
|
+
elif len(current_files) > 1:
|
|
60
|
+
status = 'multiple_files'
|
|
61
|
+
else:
|
|
62
|
+
status = 'downloadable'
|
|
63
|
+
return ResourceRecord(resource_id=resource_id, status=status, resource=resource)
|
|
64
|
+
except httpx.HTTPStatusError as e:
|
|
65
|
+
status = 'not_found' if e.response.status_code == 404 else 'fetch_error'
|
|
66
|
+
return ResourceRecord(resource_id=resource_id, status=status, error=str(e))
|
|
67
|
+
except Exception as e:
|
|
68
|
+
return ResourceRecord(resource_id=resource_id, status='fetch_error', error=str(e))
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def fetch_resource_records_batch(client: httpx.AsyncClient, resource_ids: list[str]) -> list[ResourceRecord]:
|
|
72
|
+
"""Fetch multiple resource records concurrently."""
|
|
73
|
+
if not resource_ids:
|
|
74
|
+
return []
|
|
75
|
+
return list(await asyncio.gather(
|
|
76
|
+
*(fetch_resource_record(client, rid) for rid in resource_ids)
|
|
77
|
+
))
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
async def get_resource_details(resource_id: int, headers: dict) -> dict:
|
|
81
|
+
"""Retrieve details of a specific resource from the API."""
|
|
82
|
+
url = f'{BASE_URL}/api/resources/{resource_id}'
|
|
83
|
+
async with httpx.AsyncClient(headers=headers, http2=True, timeout=30.0) as client:
|
|
84
|
+
response = await client.get(url)
|
|
85
|
+
response.raise_for_status()
|
|
86
|
+
return response.json()['resource']
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
async def fetch_watched_resources(client: httpx.AsyncClient) -> list:
|
|
90
|
+
"""Fetch all watched resources with concurrent pagination."""
|
|
91
|
+
items, total_pages = await fetch_watched_page(client, 1)
|
|
92
|
+
if total_pages <= 1:
|
|
93
|
+
return items
|
|
94
|
+
|
|
95
|
+
coros = [fetch_watched_page(client, p) for p in range(2, total_pages + 1)]
|
|
96
|
+
page_results = await asyncio.gather(*coros)
|
|
97
|
+
|
|
98
|
+
for page_items, _ in page_results:
|
|
99
|
+
items.extend(page_items)
|
|
100
|
+
|
|
101
|
+
return items
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
async def fetch_licenses(client: httpx.AsyncClient) -> list:
|
|
105
|
+
"""Fetch all user licenses with concurrent pagination."""
|
|
106
|
+
items, total_pages = await fetch_licenses_page(client, 1)
|
|
107
|
+
if total_pages <= 1:
|
|
108
|
+
return items
|
|
109
|
+
coros = [fetch_licenses_page(client, p) for p in range(2, total_pages + 1)]
|
|
110
|
+
page_results = await asyncio.gather(*coros)
|
|
111
|
+
|
|
112
|
+
for page_items, _ in page_results:
|
|
113
|
+
items.extend(page_items)
|
|
114
|
+
|
|
115
|
+
return items
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
_KISS_CACHE_TTL_SECONDS = 60
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
async def is_kiss_downloadable(headers, force_refresh: bool = False):
|
|
122
|
+
"""Check for level 2 access by checking KISS."""
|
|
123
|
+
cache = auth.get_disk_cache()
|
|
124
|
+
cache_key = "kiss"
|
|
125
|
+
|
|
126
|
+
if not force_refresh:
|
|
127
|
+
cached = cache.get(cache_key)
|
|
128
|
+
if cached is not None:
|
|
129
|
+
return bool(cached)
|
|
130
|
+
|
|
131
|
+
async with httpx.AsyncClient(headers=headers, http2=True) as client:
|
|
132
|
+
record = await fetch_resource_record(client, "4")
|
|
133
|
+
result = record.status == "downloadable"
|
|
134
|
+
cache.set(cache_key, bool(result), expire=_KISS_CACHE_TTL_SECONDS)
|
|
135
|
+
return result
|