audora-wattpad-py (1.0.0)
Installation
pip install --index-url audora-wattpad-pyAbout this package
Async Wattpad API client implementing the audora-provider-base contract
audora-wattpad-py
An async Wattpad API client for Python 3.12+, implementing the BaseProvider contract from
audora-provider-base.
Because it implements a shared contract, code written against it ports to any other provider in the family with no changes beyond the constructor.
Unofficial. Wattpad publishes no public API; this is built against community reverse-engineering documented at
Archive-WP/WattpadAPIDocumentation. Endpoints can change without notice.
Install
Both this package and audora-provider-base live in a Forgejo registry rather than on
PyPI, so the index has to be declared. With uv, add it to your project's
pyproject.toml first:
[[tool.uv.index]]
name = "forgejo"
url = "https://forge.towu.dev/api/packages/aaron/pypi/simple"
uv add audora-wattpad-py
# pip takes the index on the command line instead
pip install --extra-index-url https://forge.towu.dev/api/packages/aaron/pypi/simple \
audora-wattpad-py
Usage
import asyncio
from audora_wattpad import WattpadProvider
async def main() -> None:
async with WattpadProvider() as wp:
story = await wp.get_story("26327373")
print(story.title, "by", story.author)
# Chapter stubs arrive with get_story; load_chapters fills in the text
# in a single request via Wattpad's bulk endpoint.
await wp.load_chapters(story, with_text=True)
for chapter in story.chapters:
print(chapter.title, len(chapter.text), "chars")
asyncio.run(main())
Authenticated use:
async with WattpadProvider() as wp:
await wp.authenticate("username", "password")
library = await wp.get_library()
async for story in wp.get_collection_stories(library):
print(story.title)
# Sessions are exportable, so you log in once rather than once per process.
session = wp.export_session() # contains secrets; store it like a password
# ... later, in another process
async with WattpadProvider() as wp:
wp.restore_session(session) # 0 requests
Beyond these two snippets, every feature has a minimal runnable example in
tests/simple-tests/
— one tiny script per method, each hitting the live API and printing what it got, with
guide.md
documenting every script and the environment variables each one reads. Copy the one
closest to what you are building.
Notes specific to Wattpad
- Rate limiting. Wattpad publishes no limits. This client defaults to a conservative
2 requests/second; pass
limiter=to override.from audora_provider_base import TokenBucketLimiter WattpadProvider(limiter=TokenBucketLimiter(rate=5.0)) - Caching. No cache is configured by default. If you pass
cache=, every request except login follows the backend's policy — chapter text and the bulk story ZIP included, so a persistent backend makes re-downloading a story cost 0 requests. The library imposes no cache policy of its own, which makes backend isolation your job: do not share one backend between authenticated and anonymous clients, or between two accounts — Wattpad returns per-account data (voted,following, drafts, paid chapter text) from the same URLs it serves anonymously. - Followers cap at 2000. Wattpad refuses to page past 2000 followers. The paginator treats that as clean end-of-data rather than raising.
UNSETvsNone. A field that isUNSETwas not part of the projection you requested;Nonemeans Wattpad says there is no value. Useaudora_provider_base.is_set()when the difference matters.
Development
uv sync --extra dev
rtk ruff format . && rtk ruff check --fix . && rtk mypy src/ tests/ && rtk pytest
Testing
Offline tests need no network and no credentials:
rtk pytest -m "not live"
Live tests run against a real account and are skipped when unconfigured. Use a throwaway account — the suite writes to it.
| Variable | Purpose |
|---|---|
WATTPAD_USERNAME, WATTPAD_PASSWORD |
Account credentials. Absent → all live tests skip. The pair also feeds the contract suite's authenticate round-trip. |
WATTPAD_TOKEN |
Use an existing session token; wins over username/password and skips the login request. The authenticate round-trip test logs in with the password and Wattpad then invalidates older tokens — so a cached token is dead after any password run. |
WATTPAD_STORY_ID |
story_ref for the contract suite. Must be owned by the account and have at least one published chapter — the inline-image tests write into its first chapter. |
WATTPAD_USER |
user_ref. Defaults to the logged-in username. |
WATTPAD_LIST_ID |
A small (2–3 story) reading list owned by the account. |
WATTPAD_ADDABLE_STORY_ID |
A story that exists but is in neither WATTPAD_LIST_ID nor the account's library — the membership tests add it and remove it again, on both surfaces. Absent → those tests skip. |
WATTPAD_PART_ID |
A chapter for the simple-tests scripts to exercise (the contract suite does not read it). Defaults to the first part of WATTPAD_STORY_ID. |
WATTPAD_SEARCH_QUERY |
Query returning more than one page. Default harry potter — deliberately a query that multi-pages with or without a session language context. A generic word like the is answered from that context and can legitimately come back empty mid-suite. |
WATTPAD_FOLLOW_TARGET |
A username the follow round-trip may follow and unfollow (also used by simple-tests/follow_unfollow.py). Pick one whose follow state nobody cares about — the official wattpad account is a good choice. Absent → the follow round-trip skips. |
WATTPAD_COMMENTED_PART_ID |
Any busy public chapter whose early paragraphs carry inline comments — the paragraph-comments test reads it, never writes. Absent → that test skips. |
WATTPAD_ALLOW_WRITES |
Set to 1 to enable every mutating test. |
WATTPAD_USERNAME=... WATTPAD_PASSWORD=... WATTPAD_STORY_ID=... rtk pytest -m live
WATTPAD_ALLOW_WRITES=1 WATTPAD_USERNAME=... ... rtk pytest
rtk pytest -rs # audit what skipped, and why
Account setup the contract suite expects. Verified against a real account; each of these is a test that fails, rather than skips, if it is not met.
- Email-verified. An unverified account is refused with error 1073 on gated actions,
followamong them. - Owns
WATTPAD_STORY_ID, which needs at least one published chapter. The image tests write into the first one, so read access is not enough. - Owns
WATTPAD_LIST_ID, a small reading list. - A library spanning more than one page — more than 20 entries, so roughly 25. Below
that the whole library fits in one page, no fan-out is ever required, and
test_load_stories_refuses_fanoutfails with nothing to refuse. WATTPAD_ADDABLE_STORY_IDout of the library — the library membership tests assert it is absent before adding it, and fail rather than skip when it is already there.
One caveat on reruns: newstory is idempotent on the title, so anything that creates a
story with a fixed name will keep resolving the first one, even after it is deleted. The
suite's authoring lifecycle salts its scratch title with a uuid for exactly this reason;
a scratch script easily might not.