letterflow 0.1.0
Build and send beautiful newsletters in 3 lines of code
License: MIT
Requires Python: >=3.10
Keywords: ai, arxiv, automation, digest, email, newsletter, rss

# 📬 Letterflow

**Build and send beautiful newsletters in 3 lines of code.**

```python
from letterflow import Newsletter, sources

Newsletter("AI Digest").add_source(sources.ArXiv(["LLM"])).send("me@email.com")
```

Letterflow is a Python SDK that makes it dead simple to create automated newsletters from various content sources (arXiv, Hacker News, RSS feeds, news APIs) with AI-powered summarization.

## ✨ Features

- **🔌 Pluggable Sources** - ArXiv, Hacker News, RSS, NewsAPI, or build your own
- **🤖 AI Summarization** - Automatic summaries using OpenAI (or bring your own)
- **🎯 Relevance Filtering** - AI-powered filtering to remove false positives
- **🎨 Beautiful Templates** - Modern dark & minimal light themes built-in
- **📧 Multiple Senders** - Gmail, SMTP, or save to file for testing
- **⚙️ Config Files** - Define newsletters in YAML for easy deployment
- **🔗 Chainable API** - Fluent interface for readable code

## 📦 Installation

```bash
pip install letterflow

# With optional dependencies
pip install letterflow[all]  # Everything
pip install letterflow[arxiv,openai]  # Just ArXiv + OpenAI
```

## 🚀 Quick Start

### The 3-Line Newsletter

```python
from letterflow import Newsletter, sources

Newsletter("AI Papers").add_source(sources.ArXiv(["machine learning"])).send("me@email.com")
```

### With AI Summarization

```python
from letterflow import Newsletter, sources, summarizers, templates

newsletter = Newsletter(
    name="AI Research Weekly",
    template=templates.modern,  # Dark theme with gradients
    summarizer=summarizers.OpenAI(model="gpt-4o-mini")
)

newsletter.add_source(sources.ArXiv(["transformer", "attention"]))
newsletter.add_source(sources.HackerNews(keywords=["AI", "GPT"], min_score=50))

newsletter.filter(topic="AI research", min_relevance=0.7)
newsletter.send(to="team@company.com")
```

### From Config File

```yaml
# newsletter.yaml
name: "AI Research Digest"
template: "modern"

summarizer:
  type: "openai"
  model: "gpt-4o-mini"

sources:
  - type: "arxiv"
    keywords: ["world models", "LLM"]
    categories: ["cs.LG", "cs.CL"]
  
  - type: "hackernews"
    keywords: ["AI", "GPT"]
    min_score: 100

filter:
  topic: "AI and machine learning"
  min_relevance: 0.6
```

```python
from letterflow import Newsletter

Newsletter.from_config("newsletter.yaml").send("me@email.com")
```

## 📚 Sources

### ArXiv
```python
from letterflow.sources import ArXiv

arxiv = ArXiv(
    keywords=["LLM", "transformer"],
    categories=["cs.CL", "cs.LG"],  # Optional: filter by category
    max_results=50
)
```

### Hacker News
```python
from letterflow.sources import HackerNews

hn = HackerNews(
    keywords=["AI", "startup"],  # Optional: filter by keywords
    min_score=50,  # Minimum points
    max_results=30
)
```

### RSS Feeds
```python
from letterflow.sources import RSS

openai_blog = RSS("https://openai.com/blog/rss.xml", source_name="OpenAI Blog")
```

### NewsAPI
```python
from letterflow.sources import NewsAPI

news = NewsAPI(
    keywords=["artificial intelligence"],
    api_key="your-key"  # Or set NEWSAPI_KEY env var
)
```

### Custom Source
```python
from letterflow.sources import Source
from letterflow import Item

class MySource(Source):
    name = "My Source"
    
    def fetch(self, since=None):
        # Your logic here
        return [
            Item(
                title="Article Title",
                content="Article content...",
                url="https://example.com/article",
                source=self.name,
                published=datetime.now()
            )
        ]

newsletter.add_source(MySource())
```

## 🤖 Summarizers

### OpenAI
```python
from letterflow.summarizers import OpenAI

summarizer = OpenAI(
    model="gpt-4o-mini",  # Cost-effective
    api_key="sk-..."  # Or set OPENAI_API_KEY env var
)
```

### No-Op (Testing)
```python
from letterflow.summarizers import NoOp

summarizer = NoOp()  # Just truncates content, no API calls
```

## 🎨 Templates

```python
from letterflow import templates

# Available templates
templates.minimal  # Clean light theme
templates.modern   # Dark theme with gradients
```

## 📧 Senders

### Gmail (Recommended)
```python
from letterflow.senders import Gmail

# Set env vars: GMAIL_ADDRESS, GMAIL_APP_PASSWORD
sender = Gmail(from_name="My Newsletter")

# Or pass directly
sender = Gmail(
    address="me@gmail.com",
    app_password="xxxx xxxx xxxx xxxx"
)
```

**Setup:** [Create an App Password](https://myaccount.google.com/apppasswords) (requires 2FA)

### Console (Testing)
```python
from letterflow.senders import Console

sender = Console()  # Prints to console instead of sending
```

### File (Preview)
```python
from letterflow.senders import File

sender = File(output_dir="./newsletters")  # Saves HTML files
```

## 🔧 Environment Variables

```bash
# OpenAI (for summarization)
OPENAI_API_KEY=sk-...

# Gmail (for sending)
GMAIL_ADDRESS=you@gmail.com
GMAIL_APP_PASSWORD=xxxx xxxx xxxx xxxx

# NewsAPI (optional)
NEWSAPI_KEY=your-key
```

## 🔄 GitHub Actions

```yaml
# .github/workflows/newsletter.yml
name: Daily Newsletter

on:
  schedule:
    - cron: '0 9 * * *'  # 9 AM UTC daily
  workflow_dispatch:

jobs:
  send:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - uses: actions/setup-python@v5
        with:
          python-version: '3.11'
      
      - run: pip install letterflow[all]
      
      - run: python send_newsletter.py
        env:
          OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
          GMAIL_ADDRESS: ${{ secrets.GMAIL_ADDRESS }}
          GMAIL_APP_PASSWORD: ${{ secrets.GMAIL_APP_PASSWORD }}
```

## 🧪 Testing

```python
# Preview without sending
newsletter.preview()  # Opens in browser

# Use console sender
newsletter.send(to="test@email.com", via="console")

# Save to file
newsletter.send(to="test@email.com", via="file")
```

## 📖 API Reference

### Newsletter

```python
Newsletter(
    name: str,                    # Newsletter name
    template: Template = None,    # Email template
    summarizer: Summarizer = None # AI summarizer
)

.add_source(source, required=False)  # Add content source
.filter(topic=None, min_relevance=0.5)  # Filter by relevance
.fetch(since=None)  # Manually fetch content
.summarize()  # Manually run summarization
.render() -> str  # Get HTML
.preview(open_browser=True)  # Preview in browser
.send(to, subject=None, via=None)  # Send newsletter
```

### Item

```python
Item(
    title: str,
    content: str,
    url: str,
    source: str,
    published: datetime,
    authors: list[str] = [],
    summary: str | None = None,
    relevance_score: float | None = None,
    metadata: dict = {}
)
```

## 💡 Examples

See the [`examples/`](./examples) directory for more:

- `simple.py` - Basic 3-line newsletter
- `ai_digest.py` - Full AI research digest
- `company_newsletter.py` - Multi-source company newsletter
- `config_based.py` - YAML config approach

## 📄 License

MIT License - feel free to use in your own projects!

---

Built with ❤️ for the newsletter automation community.