audora-wattpad-py (1.0.1)

Published 2026-08-02 03:15:16 +02:00 by aaron

Installation

pip install --index-url  audora-wattpad-py

About 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.
  • UNSET vs None. A field that is UNSET was not part of the projection you requested; None means Wattpad says there is no value. Use audora_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, follow among 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_fanout fails with nothing to refuse.
  • WATTPAD_ADDABLE_STORY_ID out 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.

Requirements

Requires Python: >=3.12
Details
PyPI
2026-08-02 03:15:16 +02:00
5
Proprietary
276 KiB
Assets (2)
Versions (17) View all
1.0.1 2026-08-02
1.0.0 2026-08-02
0.13.0 2026-08-02
0.12.0 2026-08-02
0.11.2 2026-08-01