A Python library for reliable social media posting with threading support across multiple platforms (X, BlueSky, Reddit, LinkedIn)
Project description
Hydra Poster
๐ค AI-Generated Notice: This project was entirely "vibe-coded" by Anthropic's Claude Sonnet 4, demonstrating AI-powered software development. While functional and production-ready, the codebase represents a collaboration between human creativity and AI implementation.
A Python library for reliable social media posting across multiple platforms with comprehensive threading support. Designed for simplicity and use by AI coding agents, with stateless operations, automatic rollback on failures, and comprehensive pre-validation.
Table of Contents
- Features
- Platform Support
- Installation
- Quick Start
- Simple Examples
- Advanced Examples
- For AI Coding Agents
- Platform-Specific Details
- API Reference
- Development & Testing
- Error Handling
- Contributing
- License
Features
- ๐ All-or-nothing posting - Automatic rollback on failures across all platforms
- โ Pre-validation - Fail fast with comprehensive error reporting before posting
- ๐งต Threading support - Native threading on Twitter and Bluesky, numbered series on LinkedIn
- ๐ฏ Stateless operations - No persistent state between calls, perfect for AI agents
- ๐ฑ Media support - Images, videos, documents with platform-specific validation
- ๐ก๏ธ Robust error handling - Detailed exception hierarchy with actionable guidance
- ๐ Unified interface - Same
post()andpost_thread()methods across platforms - ๐ค AI-agent optimized - Simple, predictable API designed for automated usage
Platform Support
| Platform | Single Posts | Threading | Media Support | Rate Limits |
|---|---|---|---|---|
| Twitter/X | โ | โ Reply chains | Images, videos | 500 posts/month (free) |
| Bluesky | โ | โ AT Protocol | Images, videos | More permissive |
| โ | โ ๏ธ Numbered series* | Images, documents | 2s delays | |
| โ | โ No threading | None (deprecated) | Standard API limits |
*LinkedIn "threading" creates separate, unconnected posts with numbers - not true threads.
Installation
# From PyPI (when published)
pip install hydra-poster
# Development installation
git clone https://github.com/heysamtexas/hydra-poster
cd hydra-poster
make install
Quick Start
from hydra_poster import TwitterService, BlueSkyService, LinkedInService
# Basic posting
twitter = TwitterService("your_bearer_token")
result = twitter.post("Hello Twitter!")
print(f"Posted: {result.url}")
# Threading (platform-specific behavior)
bluesky = BlueSkyService("handle.bsky.social", "password")
messages = ["First post", "Second post", "Third post"]
thread_result = bluesky.post_thread(messages)
print(f"Thread: {thread_result.thread_url}")
# LinkedIn post series (NOT threading)
linkedin = LinkedInService("access_token", "person_urn")
series_result = linkedin.post_series(messages) # Creates 3 separate posts
Simple Examples
Text Posts
# Twitter
twitter = TwitterService("bearer_token")
result = twitter.post("Hello world! ๐")
# Bluesky
bluesky = BlueSkyService("username.bsky.social", "password")
result = bluesky.post("Testing from Python ๐")
# LinkedIn
linkedin = LinkedInService("access_token", "urn:li:person:12345")
result = linkedin.post("Professional update ๐ผ")
Posts with Media
from hydra_poster import MediaItem
# Single image
media = [MediaItem("/path/to/image.jpg", "image", alt_text="A beautiful sunset")]
result = twitter.post("Check this out!", media=media)
# Multiple images
media = [
MediaItem("/path/to/img1.jpg", "image", alt_text="First image"),
MediaItem("/path/to/img2.jpg", "image", alt_text="Second image")
]
result = bluesky.post("Photo gallery!", media=media)
# LinkedIn document
doc = [MediaItem("/path/to/doc.pdf", "document", alt_text="Report")]
result = linkedin.post("Quarterly report attached", media=doc)
Reddit Posts
from hydra_poster import RedditService, PostConfig
reddit = RedditService("access_token", "MyApp/1.0")
# Text post
config = PostConfig(metadata={
"subreddit": "Python",
"title": "Amazing Python Library!"
})
result = reddit.post("Check out this library...", config=config)
# Link post
config = PostConfig(metadata={
"subreddit": "programming",
"title": "GitHub Project",
"url": "https://github.com/username/repo"
})
result = reddit.post("Built something cool!", config=config)
Advanced Examples
Threading with Error Handling
from hydra_poster.exceptions import ThreadPostingError
messages = [
"๐งต Thread about AI development (1/3)",
"The technology is advancing rapidly... (2/3)",
"What are your thoughts? (3/3)"
]
try:
# Twitter creates reply chain
result = twitter.post_thread(messages, rollback_on_failure=True)
print(f"Thread created: {result.thread_url}")
print(f"Individual post URLs: {[r.url for r in result.post_results]}")
except ThreadPostingError as e:
print(f"Failed after posting {e.posted_count} messages")
print(f"Rollback attempted: {e.rollback_attempted}")
print(f"Error: {e}")
Media Validation and Error Recovery
from hydra_poster.exceptions import MediaValidationError
try:
# This will validate before posting
large_media = [MediaItem("/path/to/huge_file.mp4", "video")]
result = twitter.post("My video", media=large_media)
except MediaValidationError as e:
print(f"Media validation failed: {e}")
print("Fix suggestions:")
for suggestion in e.suggestions:
print(f" - {suggestion}")
# Retry with smaller file
small_media = [MediaItem("/path/to/small_vid.mp4", "video")]
result = twitter.post("My video (compressed)", media=small_media)
Cross-Platform Posting
services = {
'twitter': TwitterService("bearer_token"),
'bluesky': BlueSkyService("handle", "password"),
'linkedin': LinkedInService("token", "urn")
}
message = "Exciting announcement! ๐"
results = {}
failed_platforms = []
for platform, service in services.items():
try:
result = service.post(message)
results[platform] = result.url
print(f"โ
{platform}: {result.url}")
except Exception as e:
failed_platforms.append(platform)
print(f"โ {platform}: {e}")
# Handle partial failures
if failed_platforms:
print(f"Failed platforms: {failed_platforms}")
# Implement retry logic or notification
For AI Coding Agents
Installation Verification
# Always verify installation first
try:
from hydra_poster import TwitterService
print("โ
Library installed correctly")
except ImportError as e:
print(f"โ Installation failed: {e}")
exit(1)
Pre-Post Checklist
-
โ Credentials Check
import os # Verify environment variables exist bearer_token = os.getenv('TWITTER_BEARER_TOKEN') if not bearer_token: raise ValueError("TWITTER_BEARER_TOKEN not found")
-
โ Content Validation
message = "Your content here" # Twitter: 280 characters max if len(message) > 280: raise ValueError(f"Twitter message too long: {len(message)} chars") # LinkedIn: 3000 characters max if len(message) > 3000: raise ValueError(f"LinkedIn message too long: {len(message)} chars")
-
โ Media Validation
from pathlib import Path if media_path: path = Path(media_path) if not path.exists(): raise FileNotFoundError(f"Media file not found: {path}") # Check file size (5MB limit for most platforms) if path.stat().st_size > 5 * 1024 * 1024: raise ValueError("Media file too large (>5MB)")
-
โ Always Use Error Handling
from hydra_poster.exceptions import SocialMediaError try: result = service.post(message) if not result.success: print(f"Post failed: {result.error}") except SocialMediaError as e: print(f"Platform error: {e}") # Handle specific error types
DO NOT โ
- Create service instances in loops - Reuse instances
- Post without error handling - Always wrap in try/except
- Assume credential format - Always validate first
- Retry 429 errors immediately - Implement exponential backoff
- Mix up LinkedIn threading - Use
post_series()instead
Recovery Procedures
| Error Type | Solution |
|---|---|
AuthenticationError |
Check API tokens/credentials |
RateLimitError |
Wait and retry with exponential backoff |
MediaValidationError |
Check file size/format/existence |
ThreadPostingError |
Check if partial posts need cleanup |
NetworkError |
Implement retry with timeout |
Platform-Specific Details
Twitter/X - Native Reply Chains
- Connection: Each post replies to the previous post
- UI: Native thread interface with expand/collapse
- Rollback: Deletes tweets in reverse order
- Limits: 500 posts/month (free tier), 280 chars/post
- Media: Images (5MB), videos (512MB), up to 4 per post
Bluesky - AT Protocol Threading
- Connection: Posts linked via URI and CID references
- UI: Native thread interface with proper root/parent structure
- Rollback: AT Protocol delete operations
- Limits: More permissive than Twitter
- Media: Images and videos, platform-specific limits
LinkedIn - Numbered Post Series โ ๏ธ
- Connection: NONE - Posts are completely independent
- UI: No thread interface - posts scattered in feed
- Behavior: Like posting manually with added numbers
- Method: Use
post_series()notpost_thread() - Limits: 3000 chars/post, 2s delays between posts
- Media: Images, documents (PDFs, Word docs)
Reddit - Text and Link Posts
- Threading: Not supported
- Required: Subreddit and title for all posts
- Media: No longer supported (deprecated)
- Types: Text posts or link posts (with URL)
API Reference
Core Classes
SocialMediaService (Abstract Base)
def post(self, content: str, media: Optional[List[MediaItem]] = None,
config: Optional[PostConfig] = None) -> PostResult:
"""Post content to the platform."""
def post_thread(self, messages: List[str], media: Optional[List[MediaItem]] = None,
rollback_on_failure: bool = True) -> ThreadResult:
"""Post a thread/series of messages."""
def delete_post(self, post_id: str) -> bool:
"""Delete a post by ID."""
MediaItem
MediaItem(
content: str, # File path, URL, or base64 data
media_type: str, # "image", "video", "document"
alt_text: str = "", # Accessibility text
title: str = "" # Optional title
)
PostResult
class PostResult:
success: bool # Whether post succeeded
post_id: str # Platform-specific post ID
url: str # Direct URL to post
error: Optional[str] # Error message if failed
metadata: dict # Platform-specific data
ThreadResult
class ThreadResult:
success: bool # Whether thread succeeded
post_results: List[PostResult] # Individual post results
thread_url: str # URL to thread (if available)
posted_count: int # Number successfully posted
Platform Services
# Twitter
TwitterService(bearer_token: str)
# Bluesky
BlueSkyService(handle: str, password: str)
# LinkedIn
LinkedInService(access_token: str, person_urn: str)
# Reddit
RedditService(access_token: str, user_agent: str)
Development & Testing
Development Setup
# Clone and setup
git clone https://github.com/heysamtexas/hydra-poster
cd hydra-poster
make install # Install all dependencies
make test # Run fast tests
make test-all # Run all tests including slow ones
make ci # Run all quality checks
CLI Testing Tool
The repository includes a comprehensive CLI tool in dev/cli.py for testing all functionality:
# Setup config
uv run dev/cli.py init-config
uv run dev/cli.py config-check
# Test single posts
uv run dev/cli.py post twitter "Hello world!"
uv run dev/cli.py post bluesky "Testing" --image=1
uv run dev/cli.py post linkedin "Professional update" --document
# Test threading
uv run dev/cli.py post twitter "Thread test" --threaded
uv run dev/cli.py post linkedin "Series test" --threaded
# Test Reddit
uv run dev/cli.py post reddit "My post" --subreddit=test --title="Title"
# Test all platforms
uv run dev/cli.py post all "Cross-platform test" --cleanup
# See all examples
uv run dev/cli.py examples
Commands Available
make install # Install dependencies
make test # Run fast tests
make test-all # Run all tests (including slow)
make test-cov # Run tests with coverage
make lint # Check code style
make format # Format code
make type-check # Run mypy type checking
make ci # Run all CI checks
make build # Build package
make clean # Clean cache files
Error Handling
Exception Hierarchy
from hydra_poster.exceptions import *
SocialMediaError # Base exception
โโโ AuthenticationError # Invalid credentials
โโโ RateLimitError # API rate limits hit
โโโ MediaValidationError # Invalid media files
โโโ NetworkError # Connection issues
โโโ ThreadPostingError # Thread posting failures
โโโ PlatformSpecificError # Platform-specific issues
โ โโโ TwitterError
โ โโโ BlueSkyError
โ โโโ LinkedInError
โ โโโ RedditError
Common Error Patterns
# Comprehensive error handling
try:
result = service.post(content, media=media)
except AuthenticationError:
print("Check your API credentials")
except RateLimitError as e:
print(f"Rate limited. Retry after: {e.retry_after}")
except MediaValidationError as e:
print(f"Media issue: {e}")
print("Suggestions:", e.suggestions)
except NetworkError:
print("Network issue - retry later")
except SocialMediaError as e:
print(f"Platform error: {e}")
Contributing
We welcome contributions! This AI-generated project benefits from human review and enhancement.
Development Process
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Make your changes following existing code patterns
- Add tests for new functionality
- Run the full test suite (
make ci) - Commit with descriptive messages
- Push to your fork and create a Pull Request
Code Standards
- Type hints: All functions must have type annotations
- Testing: Maintain >90% code coverage
- Linting: Code must pass
ruffchecks - Documentation: Update docstrings and README for new features
Testing
- Unit tests in
tests/directory - Mark slow tests with
@pytest.mark.slow - Use the CLI tool in
dev/for manual testing - Test against real APIs carefully (use test accounts)
License
This project is licensed under the MIT License - see the LICENSE file for details.
Credits
- Primary Development: Anthropic's Claude Sonnet 4 - AI-powered software development
- Human Collaboration: Architecture design and requirements specification
- Inspiration: The need for reliable, AI-agent-friendly social media automation
โก Built with AI โข ๐ Python 3.12+ โข ๐งต Threading Support โข ๐ก๏ธ Error Recovery โข ๐ค AI-Agent Optimized
Project details
Release history Release notifications | RSS feed
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 hydra_poster-0.1.1.tar.gz.
File metadata
- Download URL: hydra_poster-0.1.1.tar.gz
- Upload date:
- Size: 645.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
8a7912d109303be134cf98d9f41e0934a200c71ee4889e8678955f4d624dbab1
|
|
| MD5 |
21adfb7e9c343b5e415eb6706f035b5e
|
|
| BLAKE2b-256 |
c1a4ae87764501825c493d428d16023b933d779867654781190375a089a543c0
|
Provenance
The following attestation bundles were made for hydra_poster-0.1.1.tar.gz:
Publisher:
publish.yml on heysamtexas/hydra-poster
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hydra_poster-0.1.1.tar.gz -
Subject digest:
8a7912d109303be134cf98d9f41e0934a200c71ee4889e8678955f4d624dbab1 - Sigstore transparency entry: 499473635
- Sigstore integration time:
-
Permalink:
heysamtexas/hydra-poster@114ea436ced80042b0e51682c0559bc63d85f12d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/heysamtexas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@114ea436ced80042b0e51682c0559bc63d85f12d -
Trigger Event:
push
-
Statement type:
File details
Details for the file hydra_poster-0.1.1-py3-none-any.whl.
File metadata
- Download URL: hydra_poster-0.1.1-py3-none-any.whl
- Upload date:
- Size: 31.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 |
353863769c16b0a14f76c326a67b488e3127feec36d11d2bef67e3022b5bbd7c
|
|
| MD5 |
b370c9e6de24aa3d4ec793b3ed264763
|
|
| BLAKE2b-256 |
48844ca7edddc64ad5e6ef0cdd79b4f70fc3651077ff3a39baeaac627261e2e3
|
Provenance
The following attestation bundles were made for hydra_poster-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on heysamtexas/hydra-poster
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
hydra_poster-0.1.1-py3-none-any.whl -
Subject digest:
353863769c16b0a14f76c326a67b488e3127feec36d11d2bef67e3022b5bbd7c - Sigstore transparency entry: 499473663
- Sigstore integration time:
-
Permalink:
heysamtexas/hydra-poster@114ea436ced80042b0e51682c0559bc63d85f12d -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/heysamtexas
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@114ea436ced80042b0e51682c0559bc63d85f12d -
Trigger Event:
push
-
Statement type: