reddigraph
Python client for ReddiGraph, the Reddit community intelligence API. Sync and async, fully typed, with retries and cursor pagination handled for you.
pip install reddigraph
Usage
from reddigraph import ReddiGraph
with ReddiGraph(api_key="rg_live_…") as api:
page = api.subreddit_feed("technology", sort="top", time="week")
for post in page.data:
print(post.score, post.title)
api_key falls back to REDDIGRAPH_API_KEY, and base_url to
REDDIGRAPH_BASE_URL and then to https://api.reddigraph.com — so only a
self-hosted or staging deployment needs to pass it. The key is sent as
Authorization: Bearer ….
Keep one client for the lifetime of your process — it reuses connections.
Async
AsyncReddiGraph mirrors ReddiGraph method for method:
from reddigraph import AsyncReddiGraph
async with AsyncReddiGraph(api_key="rg_live_…") as api:
user = await api.user("spez")
Pagination
paginate follows the after cursor and yields items, not pages:
for post in api.paginate(api.subreddit_feed, "technology", sort="new", max_pages=5):
print(post.title)
# async
async for post in api.paginate(api.subreddit_feed, "technology", max_pages=5):
...
Without max_pages it stops when the API reports no next page. Every other
argument is forwarded to each call unchanged.
Errors
from reddigraph import NotFound, RateLimited, ReddiGraphError
try:
api.post("t3_missing")
except NotFound:
...
except RateLimited as exc:
time.sleep(exc.retry_after)
except ReddiGraphError as exc:
print(exc.code, exc.status_code, exc.details)
InvalidInput, AuthenticationError, NotFound, RateLimited,
UpstreamError and TransportError all derive from ReddiGraphError. When the
server sends a request id it is kept in exc.details["request_id"] — quote it
when reporting a problem.
Arguments that could not produce a valid request — an empty batch, a name with a
slash in it — raise ValueError before anything is sent.
Retries
429, 500, 502, 503, 504 and connection failures are retried max_retries times
(default 2). A Retry-After is obeyed as sent; everything else backs off
exponentially with jitter, up to max_backoff seconds (default 20). Other 4xx
responses are never retried.
A POST is the exception to the 5xx rule: a server error can mean the write
landed and only the answer was lost, so replaying it would queue a second export
or create a second monitor. Those are retried on a 429 — which is refused before
any work is done — but never on a 5xx.
Methods
| Method | Returns |
|---|---|
search_posts(query, subreddit, sort, time, after) |
PostList |
search_subreddits(query, after) |
SubredditSearchList |
search_users(query, after) |
UserSearchList |
search_comments(query, after) |
CommentSearchList |
trending_searches() |
TrendingList |
subreddit_feed(subreddit, sort, time, after) |
PostList |
popular_feed(sort, after) |
PostList |
discover_communities(sort, after) |
Discovery |
subreddit_about(subreddit) |
Subreddit |
subreddit_rules(subreddit) |
RuleList |
subreddit_styles(subreddit) |
SubredditStyles |
subreddit_taxonomy(subreddit) |
TaxonomyList |
subreddit_highlights(subreddit) |
HighlightList |
subreddit_wiki_page(subreddit, page) |
WikiPage |
post(post_id) |
Post |
posts(post_ids) |
PostList |
post_comments(post_id, sort, limit) |
CommentList |
user(username) |
User |
user_posts(username, sort, after) |
PostList |
user_comments(username, sort, after) |
CommentList |
user_trophies(username) |
TrophyList |
analyze_subreddit(subreddit, window, deep) |
SubredditIntelligence |
analyze_conversation(post_id, sort, limit) |
ConversationIntelligence |
related_subreddits(subreddit, window, limit) |
RelatedSubreddits |
compare_subreddits(subreddits, window) |
SubredditComparison |
trends(subreddits, window, baseline, ...) |
Trends |
subreddit_history(subreddit, days) |
HistorySeries |
post_history(post_id, days) |
HistorySeries |
topic_history(topic, days) |
HistorySeries |
create_monitor(kind, name, ...) |
Monitor |
monitors() / monitor(id) |
MonitorList / Monitor |
update_monitor(id, **changes) |
Monitor |
delete_monitor(id) |
— |
monitor_events(id, since, limit) |
MonitorEventList |
add_webhook(id, url) |
Webhook |
webhooks(id) / delete_webhook(id, hook_id) |
WebhookList / — |
monitor_deliveries(id, limit) |
DeliveryList |
create_export(kind, fmt, params) |
Export |
exports() / export(id) |
ExportList / Export |
download_export(id) |
bytes |
resolve_url(url) |
ResolvedUrl |
health() |
Health |
ready() |
Health |
Names accept the r/ and u/ prefixes; post ids accept t3_abc123 or abc123.
Intelligence
Four methods answer a question rather than return a page:
week = api.analyze_subreddit("kubernetes", window="7d")
# Read the sample before the numbers.
if week.sample.coverage < 1.0:
print(f"only {week.sample.coverage:.0%} of the window was reached")
print(week.activity.posts_per_hour, week.engagement.median_post_score)
for topic in week.content.emerging_topics:
print(topic.topic, topic.growth_ratio, topic.unique_authors)
Every one of them carries window, generated_at and sample. A metric that
could not be measured is None next to a stated reason — never 0. Percentiles
are None below five observations; growth_ratio is None for a topic with no
baseline, which is what is_new is for.
analyze_subreddit(deep=False) skips op_reply_rate, the one metric costing an
extra call per sampled post.
related_subreddits scores candidates on several signals and tells you which
ones it could measure:
for row in api.related_subreddits("kubernetes").data:
print(row.subreddit, row.similarity, row.measured_signals)
A similarity resting on one signal is a weaker claim than the same number
resting on four. Aggregate audience overlap is withheld below 25 distinct
authors on either side, with the reason in
row.signals.audience_overlap_withheld_because.
Full definitions: docs/METRICS.md.
Monitors and webhooks
monitor = api.create_monitor("keyword", name="Brand watch", query="reddigraph")
hook = api.add_webhook(monitor.id, "https://example.com/hooks/reddigraph")
print(hook.secret) # shown once, here, and never again — store it now
Verify each delivery with the shipped helper rather than reimplementing it. The
two easy mistakes — comparing signatures with ==, and ignoring the timestamp —
are both silent, and both are handled below:
from reddigraph import verify_webhook
@app.post("/hooks/reddigraph")
async def hook(request):
raw = await request.body() # raw bytes; a re-encoded dict will not match
if not verify_webhook(
secret=SECRET,
body=raw,
signature=request.headers["X-ReddiGraph-Signature"],
timestamp=request.headers["X-ReddiGraph-Timestamp"],
):
return Response(status_code=401)
Events carry a deterministic id. Deduplicate on it — the same id is never sent
twice by the same monitor, and it is stable across our redeploys.
Exports
job = api.create_export("subreddit_posts", "parquet", {"subreddit": "python"})
while api.export(job.id).status in {"queued", "running"}:
time.sleep(5)
done = api.export(job.id)
if done.truncated:
print(f"{done.rows} of {done.rows_available} rows — your plan's ceiling applied")
The file is available for 24 hours; the job record outlives it, so arriving late
gets an explanation rather than a 404. download_export fetches it in one piece:
open("posts.parquet", "wb").write(api.download_export(job.id))
For a dataset too large to hold in memory, stream done.download_url yourself
instead.
Notes
- Comment lists are flat, ordered depth-first: rebuild the tree from
depthandparent_id, and usehas_moreto spot a branch Reddit truncated. discover_communitiesreturns no posts — recommended communities and topic lists only. It replaces the oldexplore_feed, which the name misdescribed.- Search results are lighter than the dedicated endpoints, and they say so in
their own types:
UserSearchResultandSubredditSearchResultdocument every field the pane does not return. Afalseon one of those means unknown, not no — fetchuser()orsubreddit_about()for the real value. - Unknown fields are preserved, so a newer server cannot break an older client.
posts()is capped by your plan, not by the SDK: 10 ids on basic, 25 on pro, 50 on ultra, 100 on mega. Over the ceiling the server answersInvalidInputwithplan,limitandreceivedinexc.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 reddigraph-1.0.0.tar.gz.
File metadata
- Download URL: reddigraph-1.0.0.tar.gz
- Upload date:
- Size: 29.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
681fe2b22bf325e8de375713db37a5de5b92062f930be84cb774c17c4762abce
|
|
| MD5 |
8734aea74172d7ba3044f57deefa5bdd
|
|
| BLAKE2b-256 |
44320004370bf242137cff63e97a97eb355d0c73d0c60f205676463a87877608
|
File details
Details for the file reddigraph-1.0.0-py3-none-any.whl.
File metadata
- Download URL: reddigraph-1.0.0-py3-none-any.whl
- Upload date:
- Size: 32.7 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/7.0.0 CPython/3.14.2
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
c985b54eac7a915d7c47016ff46581f9bc31fa1ad5c25e5b9f7086b5f00b4610
|
|
| MD5 |
cd4f2c80a81d557b88cc2b4a6f57e205
|
|
| BLAKE2b-256 |
cf5ab247e0553325a1b3c2c97adf0952554354f6f90c82d393010172a61aa3fc
|