ranobelib-python-sdk 0.7.0__tar.gz → 0.9.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.9.0}/PKG-INFO +1 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/pyproject.toml +1 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/pyproject.toml.orig +1 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/__init__.py +2 -1
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/catalog.py +72 -4
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/client.py +30 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/models.py +20 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/README.md +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/cache.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/console.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exceptions.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/__init__.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/_illustrations.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/_shared.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/epub.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/fb2.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/pdf.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/txt.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/numbering.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/py.typed +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/sdk.py +0 -0
- {ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.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.9.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
|
+
countries: list[int] | None = None,
|
|
83
85
|
sort: str = DEFAULT_SORT,
|
|
84
86
|
refresh: bool = False,
|
|
85
87
|
) -> CatalogPage:
|
|
@@ -96,7 +98,20 @@ 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
|
+
countries: ``Country.id``s to filter by. A title matches if its own country is
|
|
108
|
+
*any* of these (OR, not AND like ``genres``/``tags`` — a title only has one
|
|
109
|
+
country of origin, so requiring all of them could never match past the first,
|
|
110
|
+
see docs/api-notes.md). ``None`` or an empty list applies no country filter.
|
|
111
|
+
Ids come from ``list_countries()``. Sent on the wire as the API's ``types[]``
|
|
112
|
+
parameter, not a ``country``/``countries[]`` one — see docs/api-notes.md for
|
|
113
|
+
why (``Country`` mirrors the API's own "type" concept, which isn't strictly
|
|
114
|
+
limited to literal countries).
|
|
100
115
|
sort: Sort order. Despite the name, this is sent as the API's ``sort_by``
|
|
101
116
|
parameter — an actual ``sort`` parameter exists but is silently ignored by
|
|
102
117
|
the API (see docs/api-notes.md). Known accepted values: ``"name"``,
|
|
@@ -122,7 +137,14 @@ class Catalog:
|
|
|
122
137
|
)
|
|
123
138
|
|
|
124
139
|
key = _cache_key(
|
|
125
|
-
page=page,
|
|
140
|
+
page=page,
|
|
141
|
+
per_page=per_page,
|
|
142
|
+
query=query,
|
|
143
|
+
genres=genres,
|
|
144
|
+
tags=tags,
|
|
145
|
+
status=status,
|
|
146
|
+
countries=countries,
|
|
147
|
+
sort=sort,
|
|
126
148
|
)
|
|
127
149
|
if not refresh:
|
|
128
150
|
cached = self._cache.get(key)
|
|
@@ -130,7 +152,14 @@ class Catalog:
|
|
|
130
152
|
return _build_page(cached)
|
|
131
153
|
|
|
132
154
|
data = await self._client.list_titles(
|
|
133
|
-
page=page,
|
|
155
|
+
page=page,
|
|
156
|
+
per_page=per_page,
|
|
157
|
+
query=query,
|
|
158
|
+
genres=genres,
|
|
159
|
+
tags=tags,
|
|
160
|
+
status=status,
|
|
161
|
+
countries=countries,
|
|
162
|
+
sort=sort,
|
|
134
163
|
)
|
|
135
164
|
self._cache.set(key, data)
|
|
136
165
|
return _build_page(data)
|
|
@@ -163,6 +192,33 @@ class Catalog:
|
|
|
163
192
|
self._cache.set(key, data)
|
|
164
193
|
return _build_genres(data)
|
|
165
194
|
|
|
195
|
+
async def list_countries(self, *, refresh: bool = False) -> list[Country]:
|
|
196
|
+
"""Fetch the full list of catalog countries (id → name), for use as filter options
|
|
197
|
+
with ``list_titles(countries=...)``.
|
|
198
|
+
|
|
199
|
+
Mirrors ``list_genres()`` exactly, down to the network-wide, not-site-scoped shape
|
|
200
|
+
of the underlying endpoint (``GET /api/constants?fields[]=types`` — the API's own
|
|
201
|
+
name for what this SDK calls "country" is "type", see docs/api-notes.md and
|
|
202
|
+
``Country``'s docstring). This filters that down to entries tagged for ranobelib.me
|
|
203
|
+
before returning, same as ``list_genres()`` does for genres.
|
|
204
|
+
|
|
205
|
+
Args:
|
|
206
|
+
refresh: Bypass the disk cache and re-fetch from the API even if the country
|
|
207
|
+
list was already cached.
|
|
208
|
+
|
|
209
|
+
Returns:
|
|
210
|
+
Every ``Country`` (id, name) that applies to ranobelib.me.
|
|
211
|
+
"""
|
|
212
|
+
key = "catalog:countries"
|
|
213
|
+
if not refresh:
|
|
214
|
+
cached = self._cache.get(key)
|
|
215
|
+
if cached is not None:
|
|
216
|
+
return _build_countries(cached)
|
|
217
|
+
|
|
218
|
+
data = await self._client.list_countries()
|
|
219
|
+
self._cache.set(key, data)
|
|
220
|
+
return _build_countries(data)
|
|
221
|
+
|
|
166
222
|
|
|
167
223
|
def _cache_key(
|
|
168
224
|
*,
|
|
@@ -170,11 +226,18 @@ def _cache_key(
|
|
|
170
226
|
per_page: int,
|
|
171
227
|
query: str | None,
|
|
172
228
|
genres: list[int] | None,
|
|
229
|
+
tags: list[int] | None,
|
|
173
230
|
status: int | None,
|
|
231
|
+
countries: list[int] | None,
|
|
174
232
|
sort: str,
|
|
175
233
|
) -> str:
|
|
176
234
|
genres_part = ",".join(str(genre_id) for genre_id in genres or [])
|
|
177
|
-
|
|
235
|
+
tags_part = ",".join(str(tag_id) for tag_id in tags or [])
|
|
236
|
+
countries_part = ",".join(str(country_id) for country_id in countries or [])
|
|
237
|
+
return (
|
|
238
|
+
f"catalog:{page}:{per_page}:{query or ''}:{genres_part}:{tags_part}:"
|
|
239
|
+
f"{status}:{countries_part}:{sort}"
|
|
240
|
+
)
|
|
178
241
|
|
|
179
242
|
|
|
180
243
|
def _build_page(data: dict[str, Any]) -> CatalogPage:
|
|
@@ -186,3 +249,8 @@ def _build_page(data: dict[str, Any]) -> CatalogPage:
|
|
|
186
249
|
def _build_genres(data: list[dict[str, Any]]) -> list[Genre]:
|
|
187
250
|
site_id = int(RANOBELIB_SITE_ID)
|
|
188
251
|
return [Genre.model_validate(item) for item in data if site_id in item.get("site_ids", [])]
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def _build_countries(data: list[dict[str, Any]]) -> list[Country]:
|
|
255
|
+
site_id = int(RANOBELIB_SITE_ID)
|
|
256
|
+
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
|
+
countries: list[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,14 @@ 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
|
+
countries: ``Country.id``s to filter by. A title matches if its own country is
|
|
196
|
+
*any* of these (OR, not AND like ``genres``/``tags`` — a title only has one
|
|
197
|
+
country, confirmed against the live API, see docs/api-notes.md). Sent on the
|
|
198
|
+
wire as repeated ``types[]`` parameters — the API's own name for this concept
|
|
199
|
+
is "type", not "country" (see docs/api-notes.md).
|
|
191
200
|
sort: Forwarded as the API's ``sort_by`` parameter, not ``sort`` — the API
|
|
192
201
|
silently ignores an actual ``sort`` parameter (see docs/api-notes.md); this
|
|
193
202
|
mismatch between the SDK's public keyword and the wire parameter name is
|
|
@@ -206,8 +215,12 @@ class ApiClient:
|
|
|
206
215
|
params.append(("q", query))
|
|
207
216
|
for genre_id in genres or []:
|
|
208
217
|
params.append(("genres[]", str(genre_id)))
|
|
218
|
+
for tag_id in tags or []:
|
|
219
|
+
params.append(("tags[]", str(tag_id)))
|
|
209
220
|
if status is not None:
|
|
210
221
|
params.append(("status[]", str(status)))
|
|
222
|
+
for country_id in countries or []:
|
|
223
|
+
params.append(("types[]", str(country_id)))
|
|
211
224
|
|
|
212
225
|
response = await self._get("/manga", params=httpx.QueryParams(params))
|
|
213
226
|
self._raise_for_status(
|
|
@@ -235,6 +248,23 @@ class ApiClient:
|
|
|
235
248
|
data: list[dict[str, Any]] = response.json()["data"]["genres"]
|
|
236
249
|
return data
|
|
237
250
|
|
|
251
|
+
async def list_countries(self) -> list[dict[str, Any]]:
|
|
252
|
+
"""Fetch the raw country/type list shared across the whole lib.social network.
|
|
253
|
+
|
|
254
|
+
Same shape and caveats as ``list_genres``: not site-scoped by any request parameter,
|
|
255
|
+
returns every "type" known to the network at once, each tagged with the site ids it
|
|
256
|
+
applies to via ``site_ids``. Named ``list_countries``/``fields[]=types`` mismatch is
|
|
257
|
+
deliberate — see ``Catalog.list_countries`` and docs/api-notes.md for why this reuses
|
|
258
|
+
the API's own "type" concept rather than the unrelated ``fields[]=countries`` field.
|
|
259
|
+
|
|
260
|
+
Returns:
|
|
261
|
+
The raw ``data.types`` array from the API response, unfiltered by site.
|
|
262
|
+
"""
|
|
263
|
+
response = await self._get("/constants", params=httpx.QueryParams([("fields[]", "types")]))
|
|
264
|
+
self._raise_for_status(response, not_found=RanobeLibError("Unexpected 404 from type list"))
|
|
265
|
+
data: list[dict[str, Any]] = response.json()["data"]["types"]
|
|
266
|
+
return data
|
|
267
|
+
|
|
238
268
|
async def _get(self, url: str, *, params: Any = None) -> httpx.Response:
|
|
239
269
|
"""Issue a GET request, bounded by concurrency/pacing and retried on 429/5xx."""
|
|
240
270
|
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.9.0}/src/ranobelib/exporters/__init__.py
RENAMED
|
File without changes
|
{ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.0}/src/ranobelib/exporters/_illustrations.py
RENAMED
|
File without changes
|
{ranobelib_python_sdk-0.7.0 → ranobelib_python_sdk-0.9.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
|