audora-inkitt-py (0.0.1)
Installation
pip install --index-url audora-inkitt-pyAbout this package
Async Inkitt API client implementing the audora-provider-base contract
audora-inkitt-py
An async Inkitt 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. Inkitt publishes no API; this client is built from community reverse-engineering plus live probing, and endpoints can change without notice. Where live behaviour contradicts the documentation, live wins — every such deviation is recorded on the method that encodes it.
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-inkitt-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-inkitt-py
Usage
The whole reader surface works anonymously — story detail, chapters with full text, search, comments, reviews, reading lists:
import asyncio
from audora_inkitt import InkittProvider
async def main() -> None:
async with InkittProvider() as inkitt:
story = await inkitt.get_story(1729326)
await inkitt.load_chapters(story, with_text=True)
print(story.title, "—", len(story.chapters or []), "chapters")
async for hit in inkitt.search_stories("dragons"):
print(hit.title)
break
asyncio.run(main())
Authenticated use unlocks the account surface — recommendations, notifications, the reading-list write lifecycle, and authoring:
async with InkittProvider() as inkitt:
await inkitt.authenticate("username-or-email", "password")
lists = await inkitt.list_collections(inkitt.account_user_id()).all()
Sessions round-trip through export_session() / restore_session(). Inkitt's tokens are
stable — a fresh login returns the same token rather than invalidating older ones.
Notes specific to Inkitt
- Anonymous chapter text is a preview. Without a session,
load_chapters(..., with_text=True)carries text for only the first few chapters while still claiming access; any logged-in account (no purchase needed) receives the whole book. Checkstory.chapters_loadedrather than trusting the quota. - Ids are numeric. Usernames cannot address the user routes — resolve them through
search_usersfirst. Chapters additionally need story context: pass aChapterparsed from its story, thestory=hint, or spell both halves as"storyId:chapterId". - Rate limiting. Nothing is documented, but a limiter exists — requests are throttled to 2/s by default, and sustained multi-hundred-page walks can still meet 429s.
- Caching. Pass
cache=at your own policy; the package imposes none. A cache shared across two accounts can leak private reads between them. - Paragraph comments serve counts only. The platform never serves inline-comment
bodies to readers, so the canonical
PARAGRAPH_COMMENTScapability is not declared;get_paragraph_comment_countsexposes what exists. UNSETvsNone(base vocabulary):UNSETmeans the projection never carried the field;Nonemeans the platform explicitly said null.
Development
uv sync --extra dev
uv run ruff format --check . && uv run ruff check .
uv run mypy src/ tests/
uv run pytest
AUDORA_PROVIDER_BASE_STRICT=1 uv run pytest
The quality-gate CI runs the same chain on every push. See AGENTS.md for the
repository conventions, the architecture, and the platform invariants.
Testing
Offline tests need no network and no credentials — pytest -m "not live" (and the plain
default run) stays green on a fresh clone.
Live tests run against a real Inkitt account and are skipped when unconfigured. Use a throwaway account: with writes enabled the suite creates and deletes real content, and wall posts cannot be deleted at all.
| Variable | Purpose |
|---|---|
INKITT_USERNAME, INKITT_PASSWORD |
Account credentials (the login route accepts a username in the email field). Absent → all live tests skip. The pair also feeds the authenticate round-trip, and the wrong-password probe reuses the real INKITT_USERNAME — never an invented address: the login route is login-or-SIGNUP and would create the account. |
INKITT_TOKEN |
Use an existing session token; wins over the password and skips the login request. Pair with INKITT_USER_ID (and optionally INKITT_USERNAME) so the session knows its identity. |
INKITT_USER_ID |
The account's own numeric id — user_ref/own_user_ref, and the wall-lifecycle target (wall posts land on the account's own wall). |
INKITT_STORY_ID |
story_ref: any public story with several chapters; it need not be owned (the authoring tests build their own scratch story). Also the reading-position write target. |
INKITT_LIST_ID |
collection_ref: a reading list owned by the account. |
INKITT_ADDABLE_STORY_ID |
A story that exists but is not in INKITT_LIST_ID — the membership tests add and remove it (Inkitt rejects adding a story already in a list). |
INKITT_FOLLOW_TARGET |
A numeric user id whose follow state nobody cares about — the round trip follows and unfollows it. |
INKITT_COMMENTED_CHAPTER_ID |
The "storyId:chapterId" form — Inkitt cannot address a chapter by id alone. Keep the story half equal to INKITT_STORY_ID; the reading-position write pairs them. |
INKITT_LIKEABLE_STORY_ID |
A QUIET story for the like round trip — the only read-back is the like count, and a busy story's moving count flakes. |
INKITT_SEARCH_QUERY |
A query returning multiple pages. Default romance. |
INKITT_ALLOW_WRITES |
Set to 1 to enable every mutating test — the second gate. Holding credentials never by itself authorises writes. |
uv run pytest -m "not live" # offline only
INKITT_USERNAME=... INKITT_PASSWORD=... uv run pytest # + live reads
INKITT_ALLOW_WRITES=1 ... uv run pytest # + real writes
uv run pytest -rs # audit what skipped
Account setup the contract suite expects:
- A confirmed email — comment and wall writes answer 403
(
AccountNotConfirmedError) otherwise. - The
INKITT_LIST_IDlist exists and is owned by the account, andINKITT_ADDABLE_STORY_IDis not in it when a run starts (the tests scrub, but a crashed run can leave state). - Expect the suite to leave one wall post per writes run on the account's own wall — there is no delete route.
Run pytest -rs regularly. A silently-skipped contract test is the main failure mode of
a config-driven live suite.