ranobelib-python-sdk 0.6.1__tar.gz → 0.7.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 (22) hide show
  1. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/PKG-INFO +1 -1
  2. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/pyproject.toml +1 -1
  3. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/pyproject.toml.orig +1 -1
  4. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/__init__.py +2 -1
  5. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/catalog.py +37 -5
  6. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/client.py +18 -0
  7. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/README.md +0 -0
  8. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/cache.py +0 -0
  9. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/console.py +0 -0
  10. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exceptions.py +0 -0
  11. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exporters/__init__.py +0 -0
  12. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exporters/_illustrations.py +0 -0
  13. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exporters/_shared.py +0 -0
  14. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exporters/epub.py +0 -0
  15. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exporters/fb2.py +0 -0
  16. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exporters/pdf.py +0 -0
  17. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/exporters/txt.py +0 -0
  18. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/models.py +0 -0
  19. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/numbering.py +0 -0
  20. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/py.typed +0 -0
  21. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.0}/src/ranobelib/sdk.py +0 -0
  22. {ranobelib_python_sdk-0.6.1 → ranobelib_python_sdk-0.7.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.6.1
3
+ Version: 0.7.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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ranobelib-python-sdk"
3
- version = "0.6.1"
3
+ version = "0.7.0"
4
4
  description = "Async Python SDK for ranobelib.me: title metadata, chapter/volume downloads, and export to epub/fb2/txt/pdf"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ranobelib-python-sdk"
3
- version = "0.6.1"
3
+ version = "0.7.0"
4
4
  description = "Async Python SDK for ranobelib.me: title metadata, chapter/volume downloads, and export to epub/fb2/txt/pdf"
5
5
  readme = "README.md"
6
6
  license = "MIT"
@@ -13,7 +13,7 @@ from ranobelib.exceptions import (
13
13
  TitleNotFoundError,
14
14
  VolumeNotFoundError,
15
15
  )
16
- from ranobelib.models import CatalogPage, Chapter, ChapterBranch, Title, Volume
16
+ from ranobelib.models import CatalogPage, Chapter, ChapterBranch, Genre, Title, Volume
17
17
  from ranobelib.sdk import RanobeLib
18
18
  from ranobelib.sizing import chapter_size, volume_size
19
19
 
@@ -26,6 +26,7 @@ __all__ = [
26
26
  "ChapterBranch",
27
27
  "ChapterNotFoundError",
28
28
  "DownloadTitleInterruptedError",
29
+ "Genre",
29
30
  "MultipleTitleTranslationsError",
30
31
  "MultipleTranslationsError",
31
32
  "RanobeLib",
@@ -12,8 +12,8 @@ from types import TracebackType
12
12
  from typing import Any, Self
13
13
 
14
14
  from ranobelib.cache import DEFAULT_CACHE_DIR, DiskCache
15
- from ranobelib.client import ApiClient
16
- from ranobelib.models import CatalogPage, Title
15
+ from ranobelib.client import RANOBELIB_SITE_ID, ApiClient
16
+ from ranobelib.models import CatalogPage, 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
@@ -94,9 +94,8 @@ class Catalog:
94
94
  query: Free-text search term, matched against name/rus_name/eng_name. ``None``
95
95
  (the default) or an empty string lists without searching.
96
96
  genres: Genre ids to filter by. A title must have *all* of them, not just one
97
- (AND, not OR — see docs/api-notes.md). The SDK doesn't validate these against
98
- a known genre list — there's no public, unauthenticated way to fetch one (see
99
- docs/api-notes.md) — an unrecognized id just matches nothing.
97
+ (AND, not OR — see docs/api-notes.md). Not validated against ``list_genres()``
98
+ before sending — an unrecognized id just matches nothing rather than erroring.
100
99
  status: A single ``Title.status.id`` to filter by (e.g. ongoing vs. completed).
101
100
  sort: Sort order. Despite the name, this is sent as the API's ``sort_by``
102
101
  parameter — an actual ``sort`` parameter exists but is silently ignored by
@@ -136,6 +135,34 @@ class Catalog:
136
135
  self._cache.set(key, data)
137
136
  return _build_page(data)
138
137
 
138
+ async def list_genres(self, *, refresh: bool = False) -> list[Genre]:
139
+ """Fetch the full list of catalog genres (id → name), for use as filter options
140
+ with ``list_titles(genres=[...])``.
141
+
142
+ The underlying endpoint (``GET /api/constants?fields[]=genres``) has no site-scoping
143
+ parameter — it returns the full genre list shared across the whole lib.social
144
+ network in one shot, each tagged with the site ids it applies to (see
145
+ docs/api-notes.md). This filters that down to genres tagged for ranobelib.me before
146
+ returning, so callers don't get filter options (e.g. "Детское") that could never
147
+ match any ranobelib title.
148
+
149
+ Args:
150
+ refresh: Bypass the disk cache and re-fetch from the API even if the genre list
151
+ was already cached.
152
+
153
+ Returns:
154
+ Every ``Genre`` (id, name, adult flag) that applies to ranobelib.me.
155
+ """
156
+ key = "catalog:genres"
157
+ if not refresh:
158
+ cached = self._cache.get(key)
159
+ if cached is not None:
160
+ return _build_genres(cached)
161
+
162
+ data = await self._client.list_genres()
163
+ self._cache.set(key, data)
164
+ return _build_genres(data)
165
+
139
166
 
140
167
  def _cache_key(
141
168
  *,
@@ -154,3 +181,8 @@ def _build_page(data: dict[str, Any]) -> CatalogPage:
154
181
  items = [Title.model_validate(item) for item in data["data"]]
155
182
  meta = data["meta"]
156
183
  return CatalogPage(items=items, page=meta["current_page"], has_next_page=meta["has_next_page"])
184
+
185
+
186
+ def _build_genres(data: list[dict[str, Any]]) -> list[Genre]:
187
+ site_id = int(RANOBELIB_SITE_ID)
188
+ return [Genre.model_validate(item) for item in data if site_id in item.get("site_ids", [])]
@@ -217,6 +217,24 @@ class ApiClient:
217
217
  result: dict[str, Any] = response.json()
218
218
  return result
219
219
 
220
+ async def list_genres(self) -> list[dict[str, Any]]:
221
+ """Fetch the raw genre list shared across the whole lib.social network.
222
+
223
+ Unlike ``list_titles``, this endpoint isn't site-scoped by any request parameter
224
+ (confirmed: a ``Site-Id`` header changes nothing about the response) — it returns
225
+ every genre known to the network at once, each tagged with the site ids it applies
226
+ to via a ``site_ids`` field. Callers must filter to ``RANOBELIB_SITE_ID`` themselves
227
+ (see ``Catalog.list_genres``, which drops e.g. site-5-only genres like "Детское"
228
+ before returning).
229
+
230
+ Returns:
231
+ The raw ``data.genres`` array from the API response, unfiltered by site.
232
+ """
233
+ response = await self._get("/constants", params=httpx.QueryParams([("fields[]", "genres")]))
234
+ self._raise_for_status(response, not_found=RanobeLibError("Unexpected 404 from genre list"))
235
+ data: list[dict[str, Any]] = response.json()["data"]["genres"]
236
+ return data
237
+
220
238
  async def _get(self, url: str, *, params: Any = None) -> httpx.Response:
221
239
  """Issue a GET request, bounded by concurrency/pacing and retried on 429/5xx."""
222
240
  attempt = 0