Metadata-Version: 2.4
Name: profy-sdk
Version: 0.1.0
Summary: Profy App SDK — OAuth token management and event reporting
Project-URL: Homepage, https://profy.cn
Project-URL: Documentation, https://profy.cn/zh/documentation/sdk-guide
Project-URL: Repository, https://github.com/profy-ai/profy
Project-URL: Issues, https://github.com/profy-ai/profy/issues
License: MIT
Keywords: billing,events-api,oauth,profy,saas,sdk
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 :: Software Development :: Libraries
Classifier: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.25.0
Provides-Extra: dev
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: respx>=0.21; extra == 'dev'
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy>=2.0; extra == 'sqlalchemy'
Description-Content-Type: text/markdown

# profy-sdk

Official Python SDK for the [Profy](https://profy.cn) App platform — OAuth token management and event reporting.

## Installation

```bash
pip install profy-sdk

# With optional SQLAlchemy token storage:
pip install profy-sdk[sqlalchemy]
```

## Quick Start

```python
from profy import ProfyApp

async with ProfyApp(
    client_id="your-app-id",
    client_secret="your-app-secret",
) as profy:
    # Exchange authorization code for tokens
    token = await profy.exchange_code(code, "https://your-app.com/callback")

    # Report a billing event
    await profy.report_event("generate_report", token=token)
```

### Synchronous Usage

```python
from profy import ProfyAppSync

with ProfyAppSync(
    client_id="your-app-id",
    client_secret="your-app-secret",
) as profy:
    token = profy.exchange_code(code, redirect_uri)
    profy.report_event("generate_report", token=token)
```

## Features

- **OAuth2 Authorization Code Flow** — exchange codes, refresh tokens automatically
- **Events API** — report billing events with auto token refresh and retry on 401
- **Async + Sync** — `ProfyApp` (async) and `ProfyAppSync` (sync) clients
- **Type hints** — full type annotations, PEP 561 compatible
- **Minimal dependencies** — only `httpx`
- **Optional SQLAlchemy integration** — pre-built token model + store

## Token Persistence (Optional)

### Using SQLAlchemy

```python
from profy import ProfyApp
from profy.contrib.sqlalchemy import create_token_model, SQLAlchemyTokenStore

# 1. Create the ORM model (auto-creates table with Base.metadata.create_all())
ProfyOAuthToken = create_token_model(Base)

# 2. Create store
store = SQLAlchemyTokenStore(async_session_factory, ProfyOAuthToken)

# 3. Initialize SDK with persistence
profy = ProfyApp(
    client_id="...",
    client_secret="...",
    on_token_refresh=store.on_refresh,
)

# 4. Save after exchange
token = await profy.exchange_code(code, redirect_uri)
await store.save(token.user_id, token, scope="events:write")

# 5. Load later
saved = await store.load(user_id)
```

### Custom Storage

```python
from profy import ProfyApp, TokenData

async def persist_token(token: TokenData):
    await redis.set(f"profy:{token.user_id}", token.access_token)

profy = ProfyApp(
    client_id="...",
    client_secret="...",
    on_token_refresh=persist_token,
)
```

## API Reference

### `ProfyApp` / `ProfyAppSync`

| Method | Description |
|--------|-------------|
| `exchange_code(code, redirect_uri)` | Exchange authorization code for tokens |
| `refresh_token(refresh_token)` | Manually refresh an expired token |
| `report_event(event_name, *, token, idempotency_key?, metadata?)` | Report a billing event |

### `TokenData`

```python
@dataclass
class TokenData:
    access_token: str
    refresh_token: str
    user_id: str
    expires_at: float  # unix timestamp

    @property
    def is_expired(self) -> bool: ...
```

### Exceptions

| Class | Status | Description |
|-------|--------|-------------|
| `AuthExpired` | 401 | Token expired and refresh failed |
| `InsufficientBalance` | 402 | User has insufficient credits |
| `InvalidEvent` | 400 | Event name not configured |
| `ProfyApiError` | varies | Generic API error |

## Documentation

- [Integration Quickstart](https://profy.cn/zh/documentation/integration-quickstart)
- [SDK Guide](https://profy.cn/zh/documentation/sdk-guide)
- [API Reference](https://profy.cn/zh/api/post-token)

## License

MIT
