# linwheel

Python SDK for the [LinWheel](https://www.linwheel.io) content engine API. Analyze, reshape, refine, and schedule LinkedIn content programmatically.

Built on `httpx` + `pydantic`. Sync client with full type hints.

## Install

```bash
pip install linwheel
```

## Quick Start

```python
from linwheel import LinWheel

lw = LinWheel(api_key="lw_sk_...")

# Analyze content for LinkedIn potential
analysis = lw.analyze(text="Today I shipped a new SDK...")
# → { "linkedinFit": { "score": 8 }, "suggestedAngles": [...] }

# Reshape into angle-specific posts
result = lw.reshape(
    text="Today I shipped a new SDK...",
    angles=["field_note", "contrarian"],
    save_drafts=True,
)

# Refine a draft
result = lw.refine(text=result["posts"][0]["text"], intensity="medium")

# Approve and schedule
lw.posts.approve(post_id, approved=True)
lw.posts.schedule(post_id, scheduled_at="2026-02-24T09:00:00Z", auto_publish=True)
```

## API Reference

### Content Processing

```python
# Analyze text for LinkedIn potential — topics, angles, fit score
analysis = lw.analyze(text="...", context="buildlog")

# Reshape content through 7 angle lenses
result = lw.reshape(
    text="...",
    angles=["field_note", "contrarian", "demystification"],
    pre_edit=False,        # light-edit input first
    instructions="...",    # custom instructions
    save_drafts=True,      # persist as drafts
)

# Refine with parameterized intensity
result = lw.refine(
    text="...",
    intensity="light",     # "light" | "medium" | "heavy"
    instructions="...",
    post_type="field_note",
    save_draft=True,
)

# Split long content into a post series
result = lw.split(
    text="...",
    max_posts=4,           # 2-10, default 5
    instructions="...",
    save_drafts=True,
)
```

### Drafting

```python
# Create a manual draft
draft = lw.draft(
    full_text="Your post content...",
    hook="Opening line",
    post_type="field_note",
    approved=False,
    auto_publish=True,
    scheduled_at="2026-02-24T09:00:00Z",
)

# Create a draft with image + carousel in one call
bundle = lw.bundle(
    full_text="Your post content...",
    image_headline_text="Big Title",
    image_style_preset="dark_mode",
    carousel_slides=[{"headlineText": "Slide 1"}, {"headlineText": "Slide 2"}],
)
```

### Post Management

```python
# List posts with filters
result = lw.posts.list(approved=False, limit=10)

# Get, update, approve, schedule
post = lw.posts.get(post_id)
lw.posts.update(post_id, full_text="Updated content...")
lw.posts.approve(post_id, approved=True)
lw.posts.schedule(post_id, scheduled_at="2026-02-24T09:00:00Z")

# Generate visuals
lw.posts.image(post_id, headline_text="...", style_preset="dark_mode")
lw.posts.carousel(
    post_id,
    slides=[{"headlineText": "Title"}, {"headlineText": "Content"}],
    style_preset="accent_bar",
)
```

### Voice Profiles

Voice profiles inject your writing style into all content generation. Provide writing samples and LinWheel matches your voice.

```python
# Create a voice profile from your writing
result = lw.voice_profiles.create(
    name="My Writing Voice",
    description="Technical, direct, slightly irreverent",
    samples=[article1, article2, article3],
    is_active=True,
)

# List all profiles
result = lw.voice_profiles.list()
# → { "profiles": [...], "activeProfileId": "..." }

# Switch active profile
lw.voice_profiles.activate(profile_id)

# Delete a profile
lw.voice_profiles.delete(profile_id)
```

## Configuration

```python
lw = LinWheel(
    api_key="lw_sk_...",                           # required
    base_url="https://www.linwheel.io",            # default
    signing_secret="your-hmac-secret",             # optional, for request signing
    timeout=60.0,                                  # default: 60s
)

# Context manager support
with LinWheel(api_key="lw_sk_...") as lw:
    lw.analyze(text="...")
```

### HMAC Request Signing

If your API key has a signing secret, the SDK automatically signs every request with `X-LW-Signature`:

```
X-LW-Signature: t=<unix-seconds>,v1=<hmac-sha256-hex>
```

Canonical payload: `<timestamp>.<METHOD>.<path>.<body-sha256>`

## Error Handling

```python
from linwheel import LinWheel, LinWheelApiError, LinWheelAuthError

lw = LinWheel(api_key="lw_sk_...")

try:
    lw.analyze(text="...")
except LinWheelAuthError as e:
    # 401 — invalid or missing API key
    print(e, e.response_body)
except LinWheelApiError as e:
    # 4xx/5xx
    print(e.status_code, e, e.response_body)
```

## The 7 Content Angles

| Angle | What it does |
|-------|-------------|
| `contrarian` | Challenge conventional wisdom |
| `field_note` | Share a specific observation from experience |
| `demystification` | Break down something complex |
| `identity_validation` | Make the reader feel seen |
| `provocateur` | Be deliberately provocative |
| `synthesizer` | Connect dots across domains |
| `curious_cat` | Ask genuine questions |

## Agent Example

The end-to-end flow from raw content to a scheduled LinkedIn post:

```python
from linwheel import LinWheel

lw = LinWheel(api_key="lw_sk_...")

# 1. Feed in today's work
analysis = lw.analyze(text=daily_notes, context="buildlog")

# 2. Reshape through the best angles
top_angles = [a["angle"] for a in analysis["suggestedAngles"][:3]]
result = lw.reshape(text=daily_notes, angles=top_angles, save_drafts=True)

# 3. Refine the best draft
best = result["posts"][0]
refined = lw.refine(text=best["text"], intensity="medium", save_draft=True)

# 4. Approve and schedule for tomorrow 9am
lw.posts.approve(refined["postId"], approved=True)
lw.posts.schedule(refined["postId"], scheduled_at="2026-02-24T09:00:00Z", auto_publish=True)
```

## License

MIT
