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.
redfetch/__about__.py ADDED
@@ -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
redfetch/__init__.py ADDED
File without changes
redfetch/api.py ADDED
@@ -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