Skip to main content

Async Python library for Boosty.to API

Project description

boostylib

PyPI Python Tests License

Async Python library for the Boosty.to API. Manage subscriptions, posts, donations, and automate responses — all from Python.


Features

  • Fully async (async/await) with httpx + synchronous wrapper (SyncBoostyClient)
  • Automatic token refresh with pluggable storage backends
  • Pydantic v2 models with strict validation
  • Subscriber data with email, payments, and status
  • Fluent PostBuilder for creating posts with access control (subscription levels, donations)
  • Two-step post creation (draft → publish) matching Boosty's real API flow
  • Subscription level CRUD (create, list, delete)
  • Target/goal management (money and subscriber goals)
  • Decorator-based event system with real polling detectors for new subscribers, comments, and cancellations
  • Auto-reply to comments with threading support
  • Media uploads (images, files, video, audio)
  • In-memory caching with TTL and pluggable backends (CacheBackend protocol)
  • Configurable retry with exponential backoff and rate limiting
  • Middleware pipeline for custom request/response processing
  • Browser-like HTTP fingerprint for write operations
  • PEP 561 typed — full mypy --strict support

Installation

pip install boostylib

or with uv:

uv add boostylib

Quick Start

Getting Your Tokens

  1. Log in to boosty.to
  2. Open DevTools (F12) → Application → Local Storage → https://boosty.to
  3. Copy the auth object (access_token, refresh_token, expires_at) and _clentId (this is the device_id)

Verify a Subscription

from boostylib import BoostyClient

async with BoostyClient(access_token="your_token") as client:
    status = await client.subscriptions.verify_subscription("my_blog", "user_id")

    if status.is_subscribed:
        print(f"Subscribed: {status.level.name} (paid={status.is_paid})")
    else:
        print("Not subscribed")

Get Subscriber Emails and Payments

async with BoostyClient(access_token="...") as client:
    subscribers = await client.subscriptions.get_subscribers("my_blog")
    for sub in subscribers:
        print(f"{sub.name}: {sub.email}, payments={sub.payments}, active={sub.is_active}")

    # Async iterator for all subscribers
    async for sub in client.subscriptions.iter_subscribers("my_blog"):
        if sub.is_paid:
            print(f"Paid: {sub.name} ({sub.email}) — {sub.payments} RUB")

Create a Post for Premium Subscribers

from boostylib import BoostyClient, AuthCredentials
from boostylib.builders import PostBuilder

creds = AuthCredentials(
    access_token="...",
    refresh_token="...",
    device_id="...",
)

async with BoostyClient(credentials=creds) as client:
    levels = await client.subscriptions.get_levels("my_blog")
    premium = next(l for l in levels if l.name == "Premium")

    post = (
        PostBuilder()
        .title("Premium Only Content")
        .text("This is exclusive content for premium subscribers.")
        .image(url="https://example.com/img.png")
        .access_level(level_id=str(premium.id))
        .teaser("Subscribe to Premium to unlock this post!")
        .build()
    )

    created = await client.posts.create_post("my_blog", post)
    print(f"Post created: {created.id}")

Create a Post Unlocked by Donation

from boostylib.builders import PostBuilder

post = (
    PostBuilder()
    .title("Exclusive for Donors")
    .text("Available to anyone who donated 500+ RUB")
    .minimum_donation(amount=500, currency="RUB")
    .build()
)

Auto-Respond to Events

from boostylib import BoostyClient, EventType

client = BoostyClient(access_token="...", blog_username="my_blog")

@client.on(EventType.NEW_SUBSCRIPTION)
async def on_subscribe(event):
    await client.comments.create_comment(
        event.blog_username,
        event.welcome_post_id,
        f"Welcome to {event.level.name}, {event.user.name}!",
    )

@client.on(EventType.NEW_COMMENT)
async def on_comment(event):
    await client.comments.create_comment(
        event.blog_username, event.post_id,
        f"Thanks for commenting, {event.user.name}!",
        reply_to=str(event.comment_id),
    )

@client.on(EventType.SUBSCRIPTION_CANCELLED)
async def on_cancel(event):
    print(f"Lost subscriber: {event.user.name}")

async with client:
    await client.start_polling()

Synchronous Usage

No async/await needed — works in scripts, Django views, Jupyter notebooks:

from boostylib.sync import SyncBoostyClient

with SyncBoostyClient(access_token="...") as client:
    user = client.users.get_current_user()
    print(user.name)

    subscribers = client.subscriptions.get_subscribers("my_blog")
    for sub in subscribers:
        print(f"{sub.name}: {sub.email}")

    post = PostBuilder().title("Hello").text("World").free().build()
    client.posts.create_post("my_blog", post)

Upload a File

from pathlib import Path
from boostylib import BoostyClient
from boostylib.builders import PostBuilder

async with BoostyClient(access_token="...") as client:
    media = await client.media.upload_file(
        Path("./project_source.zip"),
        filename="project_source.zip",
    )

    post = (
        PostBuilder()
        .title("Project Sources")
        .text("Download the full source archive:")
        .file(media_id=media.id, filename=media.filename)
        .access_level(level_id="premium_level_id")
        .build()
    )
    await client.posts.create_post("my_blog", post)

Manage Subscription Levels

async with BoostyClient(access_token="...") as client:
    # Create a level
    level = await client.subscriptions.create_level(
        "my_blog", name="VIP", price=1000, description="Exclusive access"
    )
    print(f"Created: {level.name} (id={level.id})")

    # Delete a level
    await client.subscriptions.delete_level("my_blog", level.id)

Manage Goals/Targets

from boostylib.enums import TargetType

async with BoostyClient(access_token="...") as client:
    # Money goal
    goal = await client.targets.create_target(
        "my_blog", description="New equipment", target_sum=50000, target_type=TargetType.MONEY,
    )

    # Subscriber goal
    sub_goal = await client.targets.create_target(
        "my_blog", description="1000 subscribers!", target_sum=1000, target_type=TargetType.SUBSCRIBERS,
    )

    # Read back
    fetched = await client.targets.get_target(goal.id)
    print(f"{fetched.description}: {fetched.current_sum}/{fetched.target_sum}")

    # Clean up
    await client.targets.delete_target(goal.id)

Configuration

All settings can be configured via environment variables, a TOML file, or programmatically.

Environment Variables

export BOOSTY_ACCESS_TOKEN=...
export BOOSTY_REFRESH_TOKEN=...
export BOOSTY_DEVICE_ID=...
export BOOSTY_TIMEOUT=30
export BOOSTY_MAX_RETRIES=3
export BOOSTY_POLL_INTERVAL=60
export BOOSTY_DEBUG=true
export BOOSTY_CACHE_ENABLED=true

TOML Config (boosty.toml)

timeout = 30.0
max_retries = 3
poll_interval = 60.0
rate_limit_requests = 60
cache_enabled = true
cache_ttl_blog = 300
cache_ttl_levels = 600
debug = false

Programmatic

from boostylib import BoostyClient, BoostySettings

settings = BoostySettings(timeout=10.0, max_retries=5, debug=True)
client = BoostyClient(settings=settings, access_token="...")

Caching

Built-in in-memory cache with configurable TTL:

from boostylib import BoostyClient, BoostySettings

# Cache enabled by default (MemoryCache)
client = BoostyClient(access_token="...")

# Disable cache
client = BoostyClient(access_token="...", settings=BoostySettings(cache_enabled=False))

# Manual cache operations
await client.cache.clear()
await client.cache.delete("blog:my_blog:info")
await client.cache.invalidate_pattern("blog:my_blog:")

Custom cache backend (e.g. Redis):

from boostylib.cache import CacheBackend

class RedisCache:
    async def get(self, key: str) -> bytes | None: ...
    async def set(self, key: str, value: bytes, *, ttl: int | None = None) -> None: ...
    async def delete(self, key: str) -> None: ...
    async def clear(self) -> None: ...

client = BoostyClient(access_token="...", cache=RedisCache())

Token Storage

Backend Description
MemoryTokenStorage In-memory, for tests and one-shot scripts (default)
FileTokenStorage Persists to ~/.boosty/auth.json with secure file permissions
EnvTokenStorage Reads from BOOSTY_* environment variables (read-only)

Custom storage:

from boostylib.auth.storage import TokenStorage, TokenPair

class RedisTokenStorage:
    async def load(self) -> TokenPair | None: ...
    async def save(self, tokens: TokenPair) -> None: ...
    async def clear(self) -> None: ...

Middleware

from boostylib import BoostyClient
from boostylib.http.middleware import BaseMiddleware
import httpx

class LoggingMiddleware(BaseMiddleware):
    async def on_request(self, request: httpx.Request) -> httpx.Request:
        print(f"-> {request.method} {request.url}")
        return request

    async def on_response(self, response: httpx.Response) -> httpx.Response:
        print(f"<- {response.status_code}")
        return response

client = BoostyClient(access_token="...", middleware=[LoggingMiddleware()])

API Reference

Client

Property / Method Description
client.users Current user info
client.blogs Blog info, blacklist
client.posts CRUD posts (draft → publish flow), list with filters
client.comments Read, create, delete, reply to comments
client.subscriptions Levels CRUD, verification, subscribers with email/payments
client.donations Donation data from posts and subscriber payments
client.targets CRUD goals (money and subscriber targets)
client.showcase Showcase items
client.media Upload images, files, video, audio
client.bundles Bundle management
client.cache Cache manager (get, set, clear, invalidate)
client.on(EventType) Decorator to register event handlers
client.start_polling(blog) Start event polling loop
client.stop_polling() Stop polling

Subscriber Model

Field Type Description
id int User ID
name str Display name
email str Email address
level SubscriptionLevel Subscription tier
price int Subscription price
payments float Total payments made
status str active / inactive
subscribed bool Currently subscribed
on_time int Subscribe timestamp
off_time int Unsubscribe timestamp
is_active bool Property: active + subscribed
is_paid bool Property: price > 0
can_write bool Can send messages

PostBuilder

Method Description
.title(str) Set post title
.text(str) Add text block
.image(url=..., media_id=...) Add image block
.video(url=..., media_id=...) Add video block
.audio(url=..., media_id=...) Add audio block
.file(media_id=..., filename=...) Add file block
.link(url=..., title=...) Add link block
.free() Free for everyone
.access_level(level_id=...) Restrict to subscription level
.minimum_donation(amount, currency) Restrict to donors
.subscribers_only() Any paid subscriber
.teaser(str) Preview for non-subscribers
.tags(list) Set tags
.scheduled_at(datetime) Schedule for later
.build() Build PostCreateRequest

Event Types

Event Detected by Description
NEW_SUBSCRIPTION Polling New subscriber appeared
SUBSCRIPTION_CANCELLED Polling Subscriber disappeared from list
NEW_COMMENT Polling New comment on recent posts
NEW_DONATION Polling New donation (via subscriber payments diff)
SUBSCRIPTION_RENEWED Subscription renewed
SUBSCRIPTION_LEVEL_CHANGED Subscription tier changed
NEW_POST New post published

Exceptions

Exception Status Description
BoostyError Base exception
BoostyAuthError 401 Invalid/expired token
BoostyForbiddenError 403 Insufficient permissions
BoostyNotFoundError 404 Resource not found
BoostyRateLimitError 429 Rate limit exceeded
BoostyServerError 5xx Server-side error
BoostyNetworkError Connection/timeout errors

Pagination

# Manual pagination
page = await client.posts.list_posts("blog", limit=10)
for post in page.data:
    print(post.title)
# Next page: page.cursor, page.is_last

# Async iterator (auto-pagination)
async for post in client.posts.iter_posts("blog"):
    print(post.title)

Available iterators: iter_posts(), iter_donations(), iter_comments(), iter_subscribers().

Development

# Clone
git clone https://github.com/BazZziliuS/boostylib.git
cd boostylib

# Install dev dependencies
uv sync --extra dev

# Run tests
uv run pytest -m "unit or integration" -v   # no API access needed
uv run pytest -m e2e -v -s                   # requires .env with credentials

# Lint & format
uv run ruff check src/ tests/
uv run ruff format src/ tests/

# Type check
uv run mypy src/

# Build
uv build

Disclaimer

This library interacts with Boosty.to's unofficial, reverse-engineered API. Endpoints may change without notice. Use at your own risk and in accordance with Boosty's Terms of Service. This project is intended for personal and research use.

License

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

boostylib-1.0.1.tar.gz (110.9 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

boostylib-1.0.1-py3-none-any.whl (48.2 kB view details)

Uploaded Python 3

File details

Details for the file boostylib-1.0.1.tar.gz.

File metadata

  • Download URL: boostylib-1.0.1.tar.gz
  • Upload date:
  • Size: 110.9 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for boostylib-1.0.1.tar.gz
Algorithm Hash digest
SHA256 6eb439af8b60c846d084b600d094601b970b60df177225815f3aa2bd77edc1d9
MD5 9828965bd3c8d3de0aadfbbeebcbd6fe
BLAKE2b-256 8081c277317275ca84fce08eed3a81a3ce5fd200f422612e3f3557d07778cacc

See more details on using hashes here.

Provenance

The following attestation bundles were made for boostylib-1.0.1.tar.gz:

Publisher: release.yml on BazZziliuS/boostylib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file boostylib-1.0.1-py3-none-any.whl.

File metadata

  • Download URL: boostylib-1.0.1-py3-none-any.whl
  • Upload date:
  • Size: 48.2 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.7

File hashes

Hashes for boostylib-1.0.1-py3-none-any.whl
Algorithm Hash digest
SHA256 e06b6243e56d0283981a3e9451cb3865a78763fc3a878eed5ce2edc53561d59c
MD5 16d609bc0290360381e11f1a677bd0e4
BLAKE2b-256 1bd7edfdf0e4c8f4fd958b3149dff6a3f15fdbca28d4474d340c3dd2238864cd

See more details on using hashes here.

Provenance

The following attestation bundles were made for boostylib-1.0.1-py3-none-any.whl:

Publisher: release.yml on BazZziliuS/boostylib

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page