ranobelib-python-sdk 0.7.0__tar.gz → 0.8.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.
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/PKG-INFO +1 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/pyproject.toml +1 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/pyproject.toml.orig +1 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/__init__.py +2 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/catalog.py +68 -4
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/client.py +28 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/models.py +20 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/README.md +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/cache.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/console.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exceptions.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/__init__.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/_illustrations.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/_shared.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/epub.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/fb2.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/pdf.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/txt.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/numbering.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/py.typed +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/sdk.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/sizing.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: ranobelib-python-sdk
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.8.0
|
|
4
4
|
Summary: Async Python SDK for ranobelib.me: title metadata, chapter/volume downloads, and export to epub/fb2/txt/pdf
|
|
5
5
|
Keywords: ranobelib,ranobe,novel,webnovel,epub,fb2,sdk
|
|
6
6
|
Author: LLIJIYAPNIK
|
|
@@ -13,7 +13,7 @@ from ranobelib.exceptions import (
|
|
|
13
13
|
TitleNotFoundError,
|
|
14
14
|
VolumeNotFoundError,
|
|
15
15
|
)
|
|
16
|
-
from ranobelib.models import CatalogPage, Chapter, ChapterBranch, Genre, Title, Volume
|
|
16
|
+
from ranobelib.models import CatalogPage, Chapter, ChapterBranch, Country, Genre, Title, Volume
|
|
17
17
|
from ranobelib.sdk import RanobeLib
|
|
18
18
|
from ranobelib.sizing import chapter_size, volume_size
|
|
19
19
|
|
|
@@ -25,6 +25,7 @@ __all__ = [
|
|
|
25
25
|
"Chapter",
|
|
26
26
|
"ChapterBranch",
|
|
27
27
|
"ChapterNotFoundError",
|
|
28
|
+
"Country",
|
|
28
29
|
"DownloadTitleInterruptedError",
|
|
29
30
|
"Genre",
|
|
30
31
|
"MultipleTitleTranslationsError",
|
|
@@ -13,7 +13,7 @@ from typing import Any, Self
|
|
|
13
13
|
|
|
14
14
|
from ranobelib.cache import DEFAULT_CACHE_DIR, DiskCache
|
|
15
15
|
from ranobelib.client import RANOBELIB_SITE_ID, ApiClient
|
|
16
|
-
from ranobelib.models import CatalogPage, Genre, Title
|
|
16
|
+
from ranobelib.models import CatalogPage, Country, Genre, Title
|
|
17
17
|
|
|
18
18
|
DEFAULT_SORT = "last_chapter_at"
|
|
19
19
|
"""Default sort order: title's last-chapter timestamp — the closest real equivalent to
|
|
@@ -79,7 +79,9 @@ class Catalog:
|
|
|
79
79
|
per_page: int = 30,
|
|
80
80
|
query: str | None = None,
|
|
81
81
|
genres: list[int] | None = None,
|
|
82
|
+
tags: list[int] | None = None,
|
|
82
83
|
status: int | None = None,
|
|
84
|
+
country: int | None = None,
|
|
83
85
|
sort: str = DEFAULT_SORT,
|
|
84
86
|
refresh: bool = False,
|
|
85
87
|
) -> CatalogPage:
|
|
@@ -96,7 +98,17 @@ class Catalog:
|
|
|
96
98
|
genres: Genre ids to filter by. A title must have *all* of them, not just one
|
|
97
99
|
(AND, not OR — see docs/api-notes.md). Not validated against ``list_genres()``
|
|
98
100
|
before sending — an unrecognized id just matches nothing rather than erroring.
|
|
101
|
+
tags: ``Tag.id``s (from ``Title.tags``) to filter by. Same AND semantics as
|
|
102
|
+
``genres`` — a title must have *all* of them, not just any one (confirmed
|
|
103
|
+
against the live API, see docs/api-notes.md; not the OR some might expect
|
|
104
|
+
from tags being more numerous/specific than genres). Also not validated
|
|
105
|
+
before sending, same as ``genres``.
|
|
99
106
|
status: A single ``Title.status.id`` to filter by (e.g. ongoing vs. completed).
|
|
107
|
+
country: A single ``Country.id`` to filter by — a title only has one country of
|
|
108
|
+
origin, unlike ``genres``. Ids come from ``list_countries()``. Sent on the
|
|
109
|
+
wire as the API's ``types[]`` parameter, not a ``country``/``countries[]``
|
|
110
|
+
one — see docs/api-notes.md for why (``Country`` mirrors the API's own
|
|
111
|
+
"type" concept, which isn't strictly limited to literal countries).
|
|
100
112
|
sort: Sort order. Despite the name, this is sent as the API's ``sort_by``
|
|
101
113
|
parameter — an actual ``sort`` parameter exists but is silently ignored by
|
|
102
114
|
the API (see docs/api-notes.md). Known accepted values: ``"name"``,
|
|
@@ -122,7 +134,14 @@ class Catalog:
|
|
|
122
134
|
)
|
|
123
135
|
|
|
124
136
|
key = _cache_key(
|
|
125
|
-
page=page,
|
|
137
|
+
page=page,
|
|
138
|
+
per_page=per_page,
|
|
139
|
+
query=query,
|
|
140
|
+
genres=genres,
|
|
141
|
+
tags=tags,
|
|
142
|
+
status=status,
|
|
143
|
+
country=country,
|
|
144
|
+
sort=sort,
|
|
126
145
|
)
|
|
127
146
|
if not refresh:
|
|
128
147
|
cached = self._cache.get(key)
|
|
@@ -130,7 +149,14 @@ class Catalog:
|
|
|
130
149
|
return _build_page(cached)
|
|
131
150
|
|
|
132
151
|
data = await self._client.list_titles(
|
|
133
|
-
page=page,
|
|
152
|
+
page=page,
|
|
153
|
+
per_page=per_page,
|
|
154
|
+
query=query,
|
|
155
|
+
genres=genres,
|
|
156
|
+
tags=tags,
|
|
157
|
+
status=status,
|
|
158
|
+
country=country,
|
|
159
|
+
sort=sort,
|
|
134
160
|
)
|
|
135
161
|
self._cache.set(key, data)
|
|
136
162
|
return _build_page(data)
|
|
@@ -163,6 +189,33 @@ class Catalog:
|
|
|
163
189
|
self._cache.set(key, data)
|
|
164
190
|
return _build_genres(data)
|
|
165
191
|
|
|
192
|
+
async def list_countries(self, *, refresh: bool = False) -> list[Country]:
|
|
193
|
+
"""Fetch the full list of catalog countries (id → name), for use as filter options
|
|
194
|
+
with ``list_titles(country=...)``.
|
|
195
|
+
|
|
196
|
+
Mirrors ``list_genres()`` exactly, down to the network-wide, not-site-scoped shape
|
|
197
|
+
of the underlying endpoint (``GET /api/constants?fields[]=types`` — the API's own
|
|
198
|
+
name for what this SDK calls "country" is "type", see docs/api-notes.md and
|
|
199
|
+
``Country``'s docstring). This filters that down to entries tagged for ranobelib.me
|
|
200
|
+
before returning, same as ``list_genres()`` does for genres.
|
|
201
|
+
|
|
202
|
+
Args:
|
|
203
|
+
refresh: Bypass the disk cache and re-fetch from the API even if the country
|
|
204
|
+
list was already cached.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Every ``Country`` (id, name) that applies to ranobelib.me.
|
|
208
|
+
"""
|
|
209
|
+
key = "catalog:countries"
|
|
210
|
+
if not refresh:
|
|
211
|
+
cached = self._cache.get(key)
|
|
212
|
+
if cached is not None:
|
|
213
|
+
return _build_countries(cached)
|
|
214
|
+
|
|
215
|
+
data = await self._client.list_countries()
|
|
216
|
+
self._cache.set(key, data)
|
|
217
|
+
return _build_countries(data)
|
|
218
|
+
|
|
166
219
|
|
|
167
220
|
def _cache_key(
|
|
168
221
|
*,
|
|
@@ -170,11 +223,17 @@ def _cache_key(
|
|
|
170
223
|
per_page: int,
|
|
171
224
|
query: str | None,
|
|
172
225
|
genres: list[int] | None,
|
|
226
|
+
tags: list[int] | None,
|
|
173
227
|
status: int | None,
|
|
228
|
+
country: int | None,
|
|
174
229
|
sort: str,
|
|
175
230
|
) -> str:
|
|
176
231
|
genres_part = ",".join(str(genre_id) for genre_id in genres or [])
|
|
177
|
-
|
|
232
|
+
tags_part = ",".join(str(tag_id) for tag_id in tags or [])
|
|
233
|
+
return (
|
|
234
|
+
f"catalog:{page}:{per_page}:{query or ''}:{genres_part}:{tags_part}:"
|
|
235
|
+
f"{status}:{country}:{sort}"
|
|
236
|
+
)
|
|
178
237
|
|
|
179
238
|
|
|
180
239
|
def _build_page(data: dict[str, Any]) -> CatalogPage:
|
|
@@ -186,3 +245,8 @@ def _build_page(data: dict[str, Any]) -> CatalogPage:
|
|
|
186
245
|
def _build_genres(data: list[dict[str, Any]]) -> list[Genre]:
|
|
187
246
|
site_id = int(RANOBELIB_SITE_ID)
|
|
188
247
|
return [Genre.model_validate(item) for item in data if site_id in item.get("site_ids", [])]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def _build_countries(data: list[dict[str, Any]]) -> list[Country]:
|
|
251
|
+
site_id = int(RANOBELIB_SITE_ID)
|
|
252
|
+
return [Country.model_validate(item) for item in data if site_id in item.get("site_ids", [])]
|
|
@@ -170,7 +170,9 @@ class ApiClient:
|
|
|
170
170
|
per_page: int,
|
|
171
171
|
query: str | None,
|
|
172
172
|
genres: list[int] | None,
|
|
173
|
+
tags: list[int] | None,
|
|
173
174
|
status: int | None,
|
|
175
|
+
country: int | None,
|
|
174
176
|
sort: str,
|
|
175
177
|
) -> dict[str, Any]:
|
|
176
178
|
"""Fetch one page of the catalog listing/search results.
|
|
@@ -187,7 +189,12 @@ class ApiClient:
|
|
|
187
189
|
or empty omits the ``q`` parameter entirely.
|
|
188
190
|
genres: Genre ids to filter by. A title must have *all* of them (AND, not OR —
|
|
189
191
|
see docs/api-notes.md), not just any one.
|
|
192
|
+
tags: Tag ids to filter by. Same AND semantics as ``genres`` — a title must have
|
|
193
|
+
*all* of them, not just any one (see docs/api-notes.md).
|
|
190
194
|
status: A single ``Title.status.id`` to filter by.
|
|
195
|
+
country: A single ``Country.id`` to filter by. Sent on the wire as ``types[]``
|
|
196
|
+
(repeatable array parameter, but only ever given one value here) — the API's
|
|
197
|
+
own name for this concept is "type", not "country" (see docs/api-notes.md).
|
|
191
198
|
sort: Forwarded as the API's ``sort_by`` parameter, not ``sort`` — the API
|
|
192
199
|
silently ignores an actual ``sort`` parameter (see docs/api-notes.md); this
|
|
193
200
|
mismatch between the SDK's public keyword and the wire parameter name is
|
|
@@ -206,8 +213,12 @@ class ApiClient:
|
|
|
206
213
|
params.append(("q", query))
|
|
207
214
|
for genre_id in genres or []:
|
|
208
215
|
params.append(("genres[]", str(genre_id)))
|
|
216
|
+
for tag_id in tags or []:
|
|
217
|
+
params.append(("tags[]", str(tag_id)))
|
|
209
218
|
if status is not None:
|
|
210
219
|
params.append(("status[]", str(status)))
|
|
220
|
+
if country is not None:
|
|
221
|
+
params.append(("types[]", str(country)))
|
|
211
222
|
|
|
212
223
|
response = await self._get("/manga", params=httpx.QueryParams(params))
|
|
213
224
|
self._raise_for_status(
|
|
@@ -235,6 +246,23 @@ class ApiClient:
|
|
|
235
246
|
data: list[dict[str, Any]] = response.json()["data"]["genres"]
|
|
236
247
|
return data
|
|
237
248
|
|
|
249
|
+
async def list_countries(self) -> list[dict[str, Any]]:
|
|
250
|
+
"""Fetch the raw country/type list shared across the whole lib.social network.
|
|
251
|
+
|
|
252
|
+
Same shape and caveats as ``list_genres``: not site-scoped by any request parameter,
|
|
253
|
+
returns every "type" known to the network at once, each tagged with the site ids it
|
|
254
|
+
applies to via ``site_ids``. Named ``list_countries``/``fields[]=types`` mismatch is
|
|
255
|
+
deliberate — see ``Catalog.list_countries`` and docs/api-notes.md for why this reuses
|
|
256
|
+
the API's own "type" concept rather than the unrelated ``fields[]=countries`` field.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
The raw ``data.types`` array from the API response, unfiltered by site.
|
|
260
|
+
"""
|
|
261
|
+
response = await self._get("/constants", params=httpx.QueryParams([("fields[]", "types")]))
|
|
262
|
+
self._raise_for_status(response, not_found=RanobeLibError("Unexpected 404 from type list"))
|
|
263
|
+
data: list[dict[str, Any]] = response.json()["data"]["types"]
|
|
264
|
+
return data
|
|
265
|
+
|
|
238
266
|
async def _get(self, url: str, *, params: Any = None) -> httpx.Response:
|
|
239
267
|
"""Issue a GET request, bounded by concurrency/pacing and retried on 429/5xx."""
|
|
240
268
|
attempt = 0
|
|
@@ -206,6 +206,25 @@ class Tag(BaseModel):
|
|
|
206
206
|
adult: bool = False
|
|
207
207
|
|
|
208
208
|
|
|
209
|
+
class Country(BaseModel):
|
|
210
|
+
"""A title's country/region of origin.
|
|
211
|
+
|
|
212
|
+
Despite the name (matching issue #48's requested public shape), this maps onto what the
|
|
213
|
+
raw API itself calls "type" (`Title`'s raw `type` field; the catalog filter's `types[]`
|
|
214
|
+
parameter; `GET /api/constants?fields[]=types`) — not a `country`/`countries[]`/
|
|
215
|
+
`fields[]=countries` concept, which turned out to be something unrelated (see
|
|
216
|
+
docs/api-notes.md). For ranobelib.me specifically, the values are three literal
|
|
217
|
+
countries (Japan, Korea, China) plus three additional non-national origin categories the
|
|
218
|
+
site groups the same way: original English-language work, original (non-translated) web
|
|
219
|
+
novel, and fanfiction — surfaced as-is, not filtered down to "real" countries only.
|
|
220
|
+
"""
|
|
221
|
+
|
|
222
|
+
model_config = ConfigDict(populate_by_name=True)
|
|
223
|
+
|
|
224
|
+
id: int
|
|
225
|
+
name: str = Field(alias="label")
|
|
226
|
+
|
|
227
|
+
|
|
209
228
|
class Person(BaseModel):
|
|
210
229
|
"""An author or artist credited on a title."""
|
|
211
230
|
|
|
@@ -243,6 +262,7 @@ class Title(BaseModel):
|
|
|
243
262
|
summary: str | None = None
|
|
244
263
|
release_date: str | None = Field(default=None, alias="releaseDate")
|
|
245
264
|
is_licensed: bool = False
|
|
265
|
+
country: Country | None = Field(default=None, alias="type")
|
|
246
266
|
genres: list[Genre] = Field(default_factory=list)
|
|
247
267
|
tags: list[Tag] = Field(default_factory=list)
|
|
248
268
|
authors: list[Person] = Field(default_factory=list)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/__init__.py
RENAMED
|
File without changes
|
{ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/_illustrations.py
RENAMED
|
File without changes
|
{ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.8.0}/src/ranobelib/exporters/_shared.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|