Metadata-Version: 2.4
Name: flypython
Version: 0.3.1
Summary: Turn web pages into structured data and change events — extract, search, crawl, monitor, and get webhook notifications when pages change.
Project-URL: Homepage, https://flypython.com
Project-URL: Documentation, https://flypython.com/docs
Project-URL: Repository, https://github.com/flypythoncom/flypython.com
Author-email: FlyPython Team <hello@flypython.com>
License: MIT
Keywords: agent,api,browser automation,data extraction,llm,monitoring,web scraping
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.9
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.9
Requires-Dist: httpx>=0.25.0
Requires-Dist: pydantic>=2.0.0
Requires-Dist: requests>=2.28.0
Provides-Extra: async
Requires-Dist: httpx>=0.25.0; extra == 'async'
Provides-Extra: dev
Requires-Dist: build>=1.2.0; extra == 'dev'
Requires-Dist: httpx>=0.25.0; extra == 'dev'
Requires-Dist: mypy>=1.0.0; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21.0; extra == 'dev'
Requires-Dist: pytest>=7.0.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Requires-Dist: twine>=5.0.0; extra == 'dev'
Requires-Dist: types-requests>=2.28.0; extra == 'dev'
Description-Content-Type: text/markdown

# FlyPython SDK

> The Python-first web runtime for agents.

Search, extract, crawl, monitor, and interact with any website. Structured data ready for LLMs.

## Installation

```bash
pip install flypython
```

## CLI

```bash
export FLYPYTHON_API_KEY=fp_your_key_here

flypython monitor https://example.com/pricing \
  --schema '{"price":"number"}' \
  --schedule '0 */6 * * *'

flypython extract https://example.com --schema '{"title":"string"}'
flypython search "competitor pricing changes" --limit 5
flypython tasks --status active
```

For async support:

```bash
pip install flypython[async]
```

## Quick Start

```python
import flypython

# Set your API key
export FLYPYTHON_API_KEY=fp_your_key_here

# Extract structured data from any URL
result = flypython.extract(
    url="https://example.com/product",
    schema={"title": "string", "price": "number", "rating": "number"}
)
print(result.data)
# { "title": "...", "price": 29.99, "rating": 4.5 }

# Search the web
results = flypython.search("best mechanical keyboards 2025")
for r in results:
    print(r.title, r.url, r.snippet)

# Crawl multiple pages
result = flypython.crawl(
    url="https://example.com",
    max_pages=10,
    schema={"title": "string", "description": "string"}
)

# Capture a screenshot
screenshot = flypython.screenshot("https://example.com")
print(screenshot.screenshot.image_url)

# Monitor a page for changes
task = flypython.monitor(
    url="https://example.com/price",
    schema={"price": "number"},
    webhook="https://myapp.com/webhooks/price",
    schedule="0 */6 * * *"  # Every 6 hours
)
print(task.id)
```

## Client Usage

### Sync Client

```python
from flypython import FlyPythonClient

client = FlyPythonClient(api_key="fp_your_key_here")

# Extract
result = client.extract(url="https://example.com", schema={"title": "string"})

# Search
results = client.search("python tutorials")

# Crawl
result = client.crawl(url="https://example.com", max_pages=5)

# Screenshot
screenshot = client.screenshot("https://example.com")

# Browser actions + extraction in one session
result = client.interact(
    url="https://example.com/product",
    actions=[{"type": "wait", "ms": 1000}],
    extract={"price": ".price"}
)

# AI-assisted selector suggestion
suggestion = client.suggest_template(
    "Watch this product page for price and stock changes",
    url="https://example.com/product"
)

# Status history
history = client.get_status_history(task_id="task-uuid", limit=30)

# API keys
keys = client.list_api_keys()
new_key = client.create_api_key(
    name="Production Key",
    scopes=["tasks:read", "tasks:write", "extract:run"],
    usage_limit=10000,
    ip_whitelist="203.0.113.0/24"
)
client.revoke_api_key("key-uuid")
```

### Async Client

```python
import asyncio
from flypython import AsyncFlyPythonClient

async def main():
    async with AsyncFlyPythonClient(api_key="fp_your_key_here") as client:
        result = await client.extract(url="https://example.com")
        print(result.data)

        tasks = await client.list_tasks()
        for t in tasks:
            print(t.name, t.status)

        task = await client.monitor(
            url="https://competitor.com/pricing",
            schema={"price": "number"},
            notify={"slack": "https://hooks.slack.com/services/..."},
        )
        print(task.id)

asyncio.run(main())
```

## Media Download

Download images and videos during extraction:

```python
# Get image URL only (default)
result = flypython.extract(
    url="https://example.com/product",
    schema={"image": "image_url"}
)
print(result.data["image"])  # "https://example.com/product.jpg"

# Download image as base64
result = flypython.extract(
    url="https://example.com/product",
    schema={"image": "image_url"},
    download_images=True
)
print(result.data["image"]["data"])  # base64 string
print(result.data["image"]["mime_type"])  # "image/jpeg"

# Save media files to disk
result = flypython.extract_with_media(
    url="https://example.com/product",
    schema={"image": "image_url"},
    download_images=True,
    save_media=True,
    output_dir="./downloads"
)
# Saved 1 media files to ./downloads
```

## Task Management

Full CRUD for monitor tasks:

```python
# Create a task
task = flypython.create_task(
    name="Competitor Price Monitor",
    target_url="https://competitor.com/product",
    mode="monitor",
    selectors={"price": ".price", "title": "h1"},
    schedule_cron="0 */6 * * *",
    destinations=[{"type": "webhook", "config": {"url": "https://hooks.example.com/flypython"}}],
)

# List all tasks
tasks = flypython.list_tasks(status="active")

# Get a specific task
task = flypython.get_task(task.id)

# Update task
flypython.update_task(task.id, status="paused")

# Run immediately
run = flypython.run_task(task.id)

# Inspect or export a run
run_detail = flypython.get_run(run["run_id"])
rows = flypython.get_run_data(run["run_id"])
csv_text = flypython.export_run(run["run_id"], format="csv")

# Delete task
flypython.delete_task(task.id)
```

## Change Detection (Diffs)

```python
# List all detected changes
diffs = flypython.list_diffs(change_type="price_drop", limit=10)
for d in diffs.diffs:
    print(d.summary, d.confidence)

# Get specific diff detail
diff = flypython.get_diff("diff-uuid")
print(diff.field_changes)

# Compare two snapshots locally (no API call)
diff = flypython.compare_snapshots(before={"price": "100"}, after={"price": "90"})
print(diff.change_type)  # "price_change"
```

## Snapshots

```python
# List snapshots for a task
snapshots = flypython.list_snapshots(task_id="task-uuid", limit=5)

# Get the most recent snapshot
latest = flypython.get_latest_snapshot(task_id="task-uuid")
print(latest.extracted_json)
```

## Billing

```python
# Check balance
balance = flypython.get_balance()
print(balance.balance_display)  # "$5.00"
print(balance.free_calls_remaining)  # 95

# Check usage
usage = flypython.get_usage()
print(usage.estimated_cost_display)

# List transactions
transactions = flypython.list_transactions(limit=10)

# Get pricing
options = flypython.get_top_up_options()
for p in options.pricing:
    print(p.operation, p.display)
```

## Alerts

```python
# List alerts
alerts = flypython.list_alerts(limit=20)
for a in alerts:
    print(a.task_name, a.alert_type, a.severity)

# Acknowledge an alert
flypython.acknowledge_alert("alert-uuid")
```

## Templates

```python
# List official templates
templates = flypython.list_templates()

# Include community templates
templates = flypython.list_templates(include_community=True)

# Get a specific template
template = flypython.get_template("amazon-product")
print(template.selectors)

# Create a community template
template = flypython.create_template(
    id="generic-price",
    name="Generic Price",
    selectors={"price": ".price", "title": "h1"},
    sample_url="https://example.com/product"
)

# Admin-only approval, and owner/admin delete
flypython.approve_template("generic-price")
flypython.delete_template("generic-price")
```

## Browser Interaction and Selector Suggestions

```python
# Browser Rendering supports wait, click, type, scroll, select, and extraction.
result = flypython.interact(
    url="https://example.com/product",
    actions=[{"type": "wait", "ms": 1000}],
    extract={"price": ".price", "stock": ".stock"}
)

suggestion = flypython.suggest_template(
    "Monitor this product page for price and stock",
    url="https://example.com/product"
)
print(suggestion["suggested_selectors"])
```

## API Keys, Status, and Account Data

```python
keys = flypython.list_api_keys()
new_key = flypython.create_api_key(
    name="CI",
    scopes=["tasks:read", "tasks:write", "extract:run"],
    usage_limit=10000,
)
flypython.revoke_api_key(new_key["id"])

history = flypython.get_status_history(task_id="task-uuid", limit=30)
print(history.days)

exported = flypython.export_account_data()

# Guarded because this disables the user and active API keys.
flypython.delete_account(confirm=True)
```

## Webhook Verification

FlyPython signs webhooks with an `X-FlyPython-Signature: t=<timestamp>,v1=<hex>`
header. The digest is `HMAC-SHA256(secret, "<timestamp>.<raw_body>")` over the
exact raw request bytes. Verify it with:

```python
from flypython import verify_webhook

signature = request.headers.get("X-FlyPython-Signature", "")
is_valid = verify_webhook(
    payload=request.body,          # raw bytes, exactly as received
    signature=signature,           # "t=1700000000,v1=abc123..."
    secret="whsec_your_webhook_secret",
    tolerance=300,                 # max signature age in seconds; 0 skips the check
)
```

Both `FlyPythonClient.verify_webhook` and `AsyncFlyPythonClient.verify_webhook`
share this signature. Pass `tolerance=0` to disable the replay/freshness check.

## Error Handling

API errors raise subclasses of `flypython.FlyPythonError`. Each error carries
`status_code` and `detail` (the server's `{"detail": "..."}` message), and the
sync and async clients raise the same classes:

| Exception | HTTP status | Notes |
|-----------|-------------|-------|
| `AuthenticationError` | 401 | Bad credentials, or API-key auth on a dashboard-only endpoint |
| `InsufficientBalanceError` | 402 | Prepaid balance too low for the call |
| `RateLimitError` | 429 | `retry_after` holds the `Retry-After` seconds (or `None`) |
| `ValidationError` | 400 | Request failed server-side validation |
| `NotFoundError` | 404 | Resource does not exist |
| `ServerError` | 5xx | Server-side failure |
| `FlyPythonError` | any other | Base class — catch it to handle every API error |

```python
import flypython
from flypython import InsufficientBalanceError, RateLimitError

try:
    result = flypython.extract(url="https://example.com")
except InsufficientBalanceError as e:
    print("Please top up:", e.detail)
except RateLimitError as e:
    print("Rate limited, retry in", e.retry_after, "seconds")
```

## Retries

Both clients retry `429` and `5xx` responses, up to `max_retries` extra
attempts (default `2`, i.e. 3 attempts total; `0` disables retries). On `429`
the server's `Retry-After` header is honored (capped at 30 seconds); otherwise
exponential backoff (`0.5 * 2**n` seconds plus jitter) is used.

```python
from flypython import FlyPythonClient, AsyncFlyPythonClient

client = FlyPythonClient(api_key="fp_...", max_retries=4)
async_client = AsyncFlyPythonClient(api_key="fp_...", max_retries=0)
```

`top_up` and `convert_credits` are never retried, so a payment-style
operation is never submitted twice.

## Dashboard-JWT-Only Methods

The following methods call endpoints that only accept a dashboard session
JWT. The backend rejects API-key auth on them with 401
(`AuthenticationError`) — this is by design. Use them with a session token
from the dashboard, not an `fp_...` API key:

- Workspaces: `list_workspaces`, `list_workspace_members`, `invite_member`, `remove_member`
- API keys: `list_api_keys`, `create_api_key`, `revoke_api_key`
- Account / GDPR: `export_account_data`, `delete_account`
- Billing: `top_up`, `convert_credits`

## API Reference

### Core Functions

| Function | Description |
|----------|-------------|
| `extract(url, schema, ...)` | Extract structured data from a URL |
| `extract_with_media(url, ...)` | Extract and save media files |
| `search(query, limit, format)` | Search the web |
| `crawl(url, max_pages, ...)` | Crawl multiple pages |
| `screenshot(url, ...)` | Capture webpage screenshot |
| `interact(url, actions, extract, wait_for, timeout)` | Load a rendered page and extract fields |
| `suggest_template(description, url)` | Suggest monitor selectors |
| `health()` / `ready()` | Check API health/readiness |
| `monitor(url, schema, template, notify, webhook, slack_webhook, email, webhook_secret, schedule, name)` | Create a monitoring task (shorthand) |

### Task Management

| Function | Description |
|----------|-------------|
| `create_task(name, target_url, ...)` | Create a monitor task |
| `list_tasks(status, limit)` | List tasks |
| `get_task(task_id)` | Get task by ID |
| `update_task(task_id, **fields)` | Update task |
| `delete_task(task_id)` | Delete task |
| `run_task(task_id)` | Trigger execution |
| `reset_task(task_id)` | Reset paused task |
| `list_task_runs(task_id, limit)` | List execution history |
| `get_run(run_id)` | Get run detail |
| `get_run_data(run_id)` | Get extracted run rows |
| `export_run(run_id, format)` | Export run data as JSON or CSV text |

### Diffs & Snapshots

| Function | Description |
|----------|-------------|
| `list_diffs(task_id, change_type, ...)` | List detected changes |
| `get_diff(diff_id)` | Get diff details |
| `compare_snapshots(before, after)` | Local snapshot comparison |
| `list_snapshots(task_id, limit)` | List snapshots |
| `get_latest_snapshot(task_id)` | Get most recent snapshot |

### Billing & Alerts

| Function | Description |
|----------|-------------|
| `get_balance()` | Get prepaid balance |
| `get_usage()` | Get monthly usage |
| `list_transactions(limit)` | List transactions |
| `get_subscription()` | Get plan status |
| `get_top_up_options()` | Get pricing info |
| `convert_credits(credits)` | Convert credits to balance |
| `list_alerts(task_id, limit)` | List alerts |
| `acknowledge_alert(alert_id)` | Acknowledge alert |

### API Keys & Status History

| Function | Description |
|----------|-------------|
| `list_api_keys()` | List API keys |
| `create_api_key(name, scopes, usage_limit, ip_whitelist)` | Create API key |
| `revoke_api_key(key_id)` | Revoke API key |
| `get_status_history(task_id, url, from_time, to_time, limit)` | Daily aggregated run history |
| `export_account_data()` | Export current user's account data |
| `delete_account(confirm=True)` | Schedule account deletion |

### Templates & Delivery

| Function | Description |
|----------|-------------|
| `list_templates(category, include_community)` | List templates |
| `get_template(template_id)` | Get template |
| `create_template(id, name, selectors, ...)` | Create community template |
| `approve_template(template_id)` | Approve community template (admin) |
| `delete_template(template_id)` | Delete community template |
| `list_delivery_logs(task_id, ...)` | List delivery logs |
| `retry_delivery(log_id)` | Retry failed delivery |

### Utilities

| Function | Description |
|----------|-------------|
| `verify_webhook(payload, signature, secret)` | Verify HMAC signature |

## REST API

The SDK uses the FlyPython REST API under the hood. You can also call the API directly:

```bash
curl -X POST https://api.flypython.com/v1/extract \
  -H "Authorization: Bearer fp_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com",
    "schema": {"title": "string"}
  }'
```

See [API Documentation](https://flypython.com/docs) for more details.

## Requirements

- Python 3.9+
- `requests>=2.28.0`
- `pydantic>=2.0.0`
- `httpx>=0.25.0` (for async client)

## Testing

```bash
cd sdk
python3 -m venv .venv
source .venv/bin/activate
pip install -e '.[dev]'
pytest
```

## License

MIT

## Links

- [Documentation](https://flypython.com/docs)
- [API Reference](https://flypython.com/docs/api)
- [Media Download Guide](https://flypython.com/docs/media-download)
- [GitHub](https://github.com/flypython/flypython)
