Metadata-Version: 2.4
Name: feedple-sdk
Version: 1.0.0
Summary: Python SDK for Feedple AI
Project-URL: Documentation, https://github.com/Umar Bello Kanwa/feedple-sdk#readme
Project-URL: Issues, https://github.com/Umar Bello Kanwa/feedple-sdk/issues
Project-URL: Source, https://github.com/Umar Bello Kanwa/feedple-sdk
Author-email: Umar Bello Kanwa <umarbellokanwa@gmail.com>
License-Expression: MIT
License-File: LICENSE.txt
Classifier: Development Status :: 5 - Production/Stable
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3.8
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 :: Implementation :: CPython
Classifier: Programming Language :: Python :: Implementation :: PyPy
Requires-Python: >=3.8
Requires-Dist: sqlalchemy
Requires-Dist: sqlglot
Requires-Dist: websockets
Provides-Extra: dev
Requires-Dist: pytest; extra == 'dev'
Requires-Dist: sqlalchemy; extra == 'dev'
Requires-Dist: websockets; extra == 'dev'
Description-Content-Type: text/markdown

# Feedple Python SDK

[![PyPI - Version](https://img.shields.io/pypi/v/feedple-sdk.svg)](https://pypi.org/project/feedple-sdk)
[![PyPI - Python Version](https://img.shields.io/pypi/pyversions/feedple-sdk.svg)](https://pypi.org/project/feedple-sdk)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://spdx.org/licenses/MIT.html)

The official Python SDK for the [Feedple AI](https://feedple.ai) platform. Connect your database to Feedple with a single class — the SDK handles authentication, schema sync, and query execution automatically.

---

## Table of Contents

- [How It Works](#how-it-works)
- [Requirements](#requirements)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Configuration](#configuration)
- [Identity & Access Control](#identity--access-control)
- [Schema Sync](#schema-sync)
- [IR Query Execution](#ir-query-execution)
- [Connection Management](#connection-management)
- [Utilities](#utilities)
- [API Reference](#api-reference)
- [License](#license)

---

## How It Works

```
Your App                      Feedple SDK                     Feedple API
    │                              │                               │
    │  FeedpleSDK(api_key, db, …)  │                               │
    │─────────────────────────────>│                               │
    │                              │── connect() ─────────────────>│
    │                              │<─ auth.ack (session_id) ──────│
    │                              │                               │
    │                              │── schema.started ────────────>│
    │                              │── schema.data (chunks) ──────>│
    │                              │── schema.completed ──────────>│
    │                              │                               │
    │                              │<─ ir.request (IR payload) ────│
    │                              │── ir.ack ────────────────────>│
    │                              │  [executes SQL against DB]    │
    │                              │── ir.result ─────────────────>│
    │                              │                               │
```

1. **Connect & Authenticate** — Opens a persistent WebSocket connection and sends your API key. The server responds with a `session_id` used to resume after disconnects.
2. **Sync Schema** — Inspects your database (tables, columns, PKs, FKs, indexes) and sends the schema to Feedple in chunks. Re-syncs on a configurable interval; skips if nothing changed.
3. **Execute Queries** — Receives incoming IR query requests from Feedple, enforces RBAC, executes them safely against your database, and returns the results.

---

## Requirements

- Python **≥ 3.8**
- A database supported by **SQLAlchemy** (PostgreSQL, MySQL, SQLite, and others)

---

## Installation

```bash
pip install feedple-sdk
```

Install with a specific database driver:

```bash
# PostgreSQL
pip install feedple-sdk psycopg2-binary

# MySQL
pip install feedple-sdk pymysql

# SQLite (built into Python — no extra driver needed)
pip install feedple-sdk
```

---

## Quick Start

```python
from sqlalchemy import create_engine
from feedple_sdk import FeedpleSDK, Identity

# 1. Create your database engine (SQLAlchemy)
db = create_engine("postgresql+psycopg2://user:pass@localhost/mydb")

# 2. Define which tables Feedple can access
identity = Identity(
    name="production",
    all_tables=True,          # expose every table, or…
    # allowed_tables=["users", "orders", "products"],  # …restrict to specific tables
)

# 3. Initialise the SDK — it starts immediately in the background
sdk = FeedpleSDK(
    api_key="sk_live_...",
    db=db,
    identity=identity,
)

# Your application keeps running normally.
# The SDK manages the WebSocket connection on a background thread.
```

> **Note:** `FeedpleSDK.__init__` starts a background daemon thread and returns immediately. Your application does not block.

---

## Configuration

All parameters are keyword-only.

```python
sdk = FeedpleSDK(
    # Required
    api_key="sk_live_...",        # Your Feedple API key
    db=engine,                    # SQLAlchemy Engine
    identity=identity,            # Identity (see below)

    # Schema sync
    auto_sync=True,               # Sync schema on startup and periodically (default: True)
    sync_interval=60,             # Seconds between sync cycles (default: 60)

    # Connection
    reconnect_enabled=True,       # Reconnect on disconnect (default: True)
    max_retries=None,             # Max reconnect attempts; None = unlimited (default: None)
    probe_before_connect=False,   # HTTP probe before WS handshake for clearer errors (default: False)
)
```

### Parameter Reference

| Parameter | Type | Default | Description |
|-----------|------|---------|-------------|
| `api_key` | `str` | **required** | Your Feedple API key. Raises `ValueError` if empty. |
| `db` | `Engine` | **required** | SQLAlchemy database engine. |
| `identity` | `Identity` | **required** | Controls which tables are accessible. |
| `auto_sync` | `bool` | `True` | Periodically re-inspect and send the schema. |
| `sync_interval` | `int` | `60` | Seconds between schema re-sync cycles. |
| `reconnect_enabled` | `bool` | `True` | Automatically reconnect on connection loss. |
| `max_retries` | `int \| None` | `None` | Cap on reconnect attempts. `None` means unlimited. |
| `probe_before_connect` | `bool` | `False` | Perform an HTTP GET before the WS handshake to surface clearer server error messages (e.g. 403 bodies). |

---

## Identity & Access Control

`Identity` controls which database tables Feedple can see and query.

```python
from feedple_sdk import Identity

# Grant access to all tables
admin_identity = Identity(
    name="admin",
    all_tables=True,
)

# Restrict to specific tables only
restricted_identity = Identity(
    name="analytics-service",
    allowed_tables=["users", "orders", "products", "events"],
    all_tables=False,   # default
)
```

### Identity Fields

| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `name` | `str \| None` | — | Human-readable label for this identity. |
| `allowed_tables` | `list[str]` | `[]` | Tables this identity may access. Ignored when `all_tables=True`. |
| `all_tables` | `bool` | `False` | When `True`, all current and future tables are accessible. |

> **Security:** The SDK enforces RBAC on every IR query request. If the IR references a table not in `allowed_tables`, a `PermissionError` is raised and an `ir.error` is returned to the server — the query never reaches the database.

---

## Schema Sync

The SDK automatically syncs your schema on startup and then every `sync_interval` seconds. You can also trigger a sync manually:

```python
import asyncio

# Manually trigger a schema sync (async — must be called from an async context)
asyncio.run(sdk.sync_schema())
```

### What gets synced

For each table the identity can access, the SDK sends:

- **Columns** — name, type string, nullable flag, default value
- **Primary key** — constrained column names
- **Foreign keys** — local columns, referenced table, referenced columns
- **Indexes** — name, column names, unique flag
- **Unique constraints** — name, column names

### Sensitive column filtering

The following column names are **never sent** to Feedple, regardless of the identity setting:

`password`, `token`, `secret`, `hash`, `salt`, `ssn`, `credit_card`

You can call `filter_sensitive_columns` manually:

```python
from feedple_sdk.core.schema_services import filter_sensitive_columns

safe_schema = filter_sensitive_columns(raw_schema)
```

---

## IR Query Execution

The SDK receives IR (Intermediate Representation) query objects from the Feedple server and executes them against your database. You do not call this yourself — it is invoked automatically via the WebSocket.

### IR Schema

```python
ir = {
    "operation": "query",            # always "query"
    "table": "orders",               # primary FROM table
    "fields": [                      # columns to SELECT
        {"column": "orders.id",     "expression": None,    "alias": None},
        {"column": "orders.amount", "expression": "sum",   "alias": "total"},
        {"column": "orders.user_id","expression": "count(distinct)", "alias": "unique_users"},
    ],
    "joins": [                       # JOIN clauses
        {
            "table":     "users",
            "on_left":   "orders.user_id",
            "on_right":  "users.id",
            "join_type": "INNER",    # "INNER" or "LEFT"
        }
    ],
    "filters": [                     # WHERE conditions (ANDed together)
        {"column": "orders.status", "operator": "eq",  "value": "active"},
        {"column": "orders.amount", "operator": "gte", "value": 100},
    ],
    "group_by":  ["orders.status"],
    "having":    [{"column": "orders.id", "operator": "gt", "value": 5}],
    "order_by":  ["orders.created_at DESC"],
    "limit":     100,
    "offset":    0,
}
```

### Supported filter operators

| Operator | Aliases | SQL |
|----------|---------|-----|
| `eq` | `=` | `col = ?` |
| `neq` | `!=` | `col != ?` |
| `gt` | `>` | `col > ?` |
| `gte` | `>=` | `col >= ?` |
| `lt` | `<` | `col < ?` |
| `lte` | `<=` | `col <= ?` |
| `in` | `in_` | `col IN (?, ?, …)` |
| `not_in` | `not in` | `col NOT IN (?, ?, …)` |
| `is_null` | — | `col IS NULL` |
| `is_not_null` | — | `col IS NOT NULL` |
| `like` | — | `col LIKE ?` |
| `ilike` | — | `col ILIKE ?` |

### Supported aggregate expressions

`count`, `count(distinct)`, `sum`, `avg`, `min`, `max`

---

## Connection Management

### Reconnect behaviour

The SDK reconnects automatically with exponential back-off:

| Attempt | Delay |
|---------|-------|
| 1 | 5 s |
| 2 | 10 s |
| 3 | 20 s |
| 4+ | 40 s → capped at 60 s |

Authentication failures (`auth.error`) are **not** retried — the SDK stops immediately and logs the error.

### Session resume

The `session_id` received in `auth.ack` is stored and re-sent on every reconnect attempt. The server resumes the session if it is still within TTL, or issues a new session ID if it has expired.

### Stopping the SDK

```python
sdk.stop()
```

Signals the background thread to stop, closes the WebSocket, and halts the event loop. Idempotent — safe to call multiple times.

---

## Utilities

### SQLCompiler

Validate and RBAC-check raw SQL strings before executing them yourself:

```python
compiler = sdk._build_compiler()

# Raises PermissionError if the SQL references a denied table
safe_sql = compiler.compile("SELECT id, name FROM users WHERE active = 1")
```

### Schema utilities

```python
from feedple_sdk.core.schema_services import (
    get_schema,
    generate_schema_hash,
    should_sync_schema,
    filter_sensitive_columns,
)

schema = get_schema(db=engine, identity=identity)
hash_  = generate_schema_hash(schema)
changed = should_sync_schema(old_schema, new_schema)
```

---

## API Reference

### `FeedpleSDK`

```python
class FeedpleSDK:
    def __init__(
        self,
        *,
        api_key: str,
        db: Engine,
        identity: Identity,
        auto_sync: bool = True,
        sync_interval: int = 60,
        reconnect_enabled: bool = True,
        max_retries: Optional[int] = None,
        probe_before_connect: bool = False,
    ) -> None: ...

    async def sync_schema(self) -> None: ...
    def stop(self) -> None: ...
```

### `Identity`

```python
@dataclass
class Identity:
    name: Optional[str]
    allowed_tables: List[str] = field(default_factory=list)
    all_tables: bool = False
```

### `PolicyEngine`

```python
class PolicyEngine:
    def __init__(self, identity: Identity): ...
    def can_access_table(self, table: str) -> bool: ...
    def validate_ir_access(self, ir: dict) -> None: ...
```

### `SQLCompiler`

```python
class SQLCompiler:
    def __init__(self, policy: PolicyEngine, dialect: str = "postgres"): ...
    def parse(self, sql: str) -> exp.Expression: ...
    def extract_tables(self, ast: exp.Expression) -> List[str]: ...
    def validate_access(self, tables: List[str]) -> None: ...
    def compile(self, sql: str) -> str: ...
```

---

## Environment URLs

By default the production release SDK targets the official Feedple AI production cluster (`https://feedple-ai-614817435356.us-central1.run.app`).

To switch to a local development environment during SDK development, set `DEV_ENV = True` in `src/feedple_sdk/core/request.py`:

```python
DEV_ENV = True  # uses http://localhost:8000/api/v1 and ws://localhost:8000/api/v1/tenants/ws
```

---

## License

`feedple-sdk` is distributed under the terms of the [MIT](https://spdx.org/licenses/MIT.html) license.
