Async Python library for Boosty.to API
Project description
boostylib
Async Python library for the Boosty.to API. Manage subscriptions, posts, donations, and automate responses — all from Python.
Features
- Fully async (
async/await) withhttpx+ synchronous wrapper (SyncBoostyClient) - Automatic token refresh with pluggable storage backends
- Pydantic v2 models with strict validation
- Subscriber data with email, payments, and status
- Fluent
PostBuilderfor 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 (
CacheBackendprotocol) - 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 --strictsupport
Installation
pip install boostylib
or with uv:
uv add boostylib
Quick Start
Getting Your Tokens
- Log in to boosty.to
- Open DevTools (
F12) → Application → Local Storage →https://boosty.to - Copy the
authobject (access_token,refresh_token,expires_at) and_clentId(this is thedevice_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 tracking with date filtering |
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
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file boostylib-0.1.0.tar.gz.
File metadata
- Download URL: boostylib-0.1.0.tar.gz
- Upload date:
- Size: 110.7 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ca3e9ed5c93887167c3585e8b3df647ef9f575d26c99bb58f471a4ae2bae4096
|
|
| MD5 |
0f4d1454b9748114d2caeefb440657fc
|
|
| BLAKE2b-256 |
79cb7839360d2064c2e1826b9ea6c4b3b3091b1d748f4e8b5a5a8f35ba30e3b9
|
Provenance
The following attestation bundles were made for boostylib-0.1.0.tar.gz:
Publisher:
release.yml on BazZziliuS/boostylib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boostylib-0.1.0.tar.gz -
Subject digest:
ca3e9ed5c93887167c3585e8b3df647ef9f575d26c99bb58f471a4ae2bae4096 - Sigstore transparency entry: 1152910472
- Sigstore integration time:
-
Permalink:
BazZziliuS/boostylib@d39bb62660d258d756fe93bb9693c9efd287c735 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/BazZziliuS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d39bb62660d258d756fe93bb9693c9efd287c735 -
Trigger Event:
push
-
Statement type:
File details
Details for the file boostylib-0.1.0-py3-none-any.whl.
File metadata
- Download URL: boostylib-0.1.0-py3-none-any.whl
- Upload date:
- Size: 47.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
157e2481d2a1152b023771533e7bb266fe8afdf2784554740fff0b4acfd14875
|
|
| MD5 |
5e699aa38b3fe4cb65b37611166db94d
|
|
| BLAKE2b-256 |
2eaf665cfabf7b14cc90bff19ea952b1ee32390a3ad0719fbfe15795684276f1
|
Provenance
The following attestation bundles were made for boostylib-0.1.0-py3-none-any.whl:
Publisher:
release.yml on BazZziliuS/boostylib
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
boostylib-0.1.0-py3-none-any.whl -
Subject digest:
157e2481d2a1152b023771533e7bb266fe8afdf2784554740fff0b4acfd14875 - Sigstore transparency entry: 1152910590
- Sigstore integration time:
-
Permalink:
BazZziliuS/boostylib@d39bb62660d258d756fe93bb9693c9efd287c735 -
Branch / Tag:
refs/tags/v0.1.0 - Owner: https://github.com/BazZziliuS
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@d39bb62660d258d756fe93bb9693c9efd287c735 -
Trigger Event:
push
-
Statement type: