Metadata-Version: 2.4
Name: polyester-sdk
Version: 0.1.0a22
Summary: Official Python SDK for Polyester APIs.
Project-URL: Homepage, https://github.com/Fabric-Labs/polyester-sdk-python
Project-URL: Documentation, https://polyester.ai/docs
Project-URL: Repository, https://github.com/Fabric-Labs/polyester-sdk-python
Project-URL: Issues, https://github.com/Fabric-Labs/polyester-sdk-python/issues
Author: Fabric Blockchain Labs Inc.
License: Proprietary
License-File: LICENSE
Keywords: connectrpc,polyester,protobuf,sdk,trading
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: Other/Proprietary License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.11
Requires-Dist: base58>=2.1.1
Requires-Dist: connectrpc<0.11,>=0.9.0
Requires-Dist: cryptography>=42
Requires-Dist: eth-abi>=5.1
Requires-Dist: eth-account>=0.13
Requires-Dist: eth-hash[pycryptodome]>=0.7
Requires-Dist: eth-utils>=5.0
Requires-Dist: httpx>=0.27
Requires-Dist: msgspec>=0.18
Requires-Dist: protobuf>=7.35.0
Requires-Dist: websockets>=15
Provides-Extra: chain
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: mypy>=1.13; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest-cov>=5; extra == 'dev'
Requires-Dist: pytest-httpserver>=1.1; extra == 'dev'
Requires-Dist: pytest>=8; extra == 'dev'
Requires-Dist: python-dotenv>=1.0; extra == 'dev'
Requires-Dist: ruff>=0.8; extra == 'dev'
Requires-Dist: twine>=5; extra == 'dev'
Provides-Extra: realtime
Description-Content-Type: text/markdown

# Polyester Python SDK

Official Python SDK for Polyester APIs, built for trading bots, backend jobs,
research notebooks, and automation.

**Status:** Alpha (`0.1.0a22`). Proprietary license (not open source).
API-key only; no browser login or JWT flows.

Requires **Python 3.11+**.

## Supported surface

<!-- sdk-capabilities:start -->
<!-- Generated by scripts/check_sdk_coverage.py --write-capabilities. Do not edit. -->

| Capability | Supported |
| --- | --- |
| Public market data (spot config, trades, candles) | Yes |
| Order book snapshot + realtime | Yes |
| Market overview (list + subscribe) | Yes |
| Order book heatmap | Yes |
| API-key (Ed25519 signature) auth | Yes |
| Wallet / browser login | No |
| Session MFA enrollment and challenges | No |
| Profile (identity subscribe) | Yes |
| API keys (list/get/subscribe/local keypair generation) | Yes |
| Subaccounts (list/get/members/invites/activity/subscribe) | Yes |
| Address book (list/view/subscribe) | Yes |
| Policies (realtime subscribe) | Yes |
| Guard signer | Yes |
| Balances, holds, equity history | Yes |
| Orders (create, cancel, modify, batch, cancel-all) | Yes |
| User trades | Yes |
| Triggers | Yes |
| Internal transfers | Yes |
| Transfer history | Yes |
| Deposit addresses | Yes |
| Trading / funding withdraws | Yes |
| Zipper deposit-withdraw config | Yes |
| Chain analytics | Yes |
| Lifecycle flows | Yes |
| Polychart / layout / whiteboard | Yes |
| Realtime account and market streams | Yes |
| Reference catalogs + wait-for-ready | Yes |
| Qty / price decimal + scaled-int inputs | Yes |
| Social verification | Yes |
| Account resolve / lookup | No |

Rows marked **No** are intentional for API-key SDKs (use the TypeScript
browser client for wallet login and session MFA).

Full cross-language comparison:
[SDK capability matrix](https://polyester.ai/docs/developer-docs/getting-started/sdk-capability-matrix).
<!-- sdk-capabilities:end -->

Rows marked **Yes** mean that an SDK wrapper exists; deployment authorization
still applies. In particular, whiteboard/social-verification and some
layout/polychart routes may require a JWT session or may not be mounted.
Private streams require an Account ID and the corresponding API-key permission.
A successful subscribe call means the token exchange and realtime handshake
completed. Treat a structured permission denial as non-transient and update the
API-key policy before retrying.

## Install

PyPI: https://pypi.org/project/polyester-sdk/

```bash
pip install "polyester-sdk==0.1.0a22"
```

Realtime (Centrifugo) and on-chain Funding helpers are included by default.

For development from a git checkout:

```bash
git clone https://github.com/Fabric-Labs/polyester-sdk-python.git
cd polyester-sdk-python
python3.11 -m venv .venv
source .venv/bin/activate
pip install -e ".[dev]"
```

## Quickstart

Create an API key in the Polyester app (**API** in the sidebar). Copy the key id
and private key when shown. The private key is only displayed once.
Open the key's **Permissions**, enable **Spot trading**, select the markets it
may trade, and set a maximum order size appropriate for the strategy.

```python
import asyncio

from polyester import AsyncPolyester

async def main() -> None:
    async with AsyncPolyester(
        api_key_id="ak_...",           # from API key creation
        api_private_key="...",       # 64-char hex secret from API key creation
        default_account_id="...",    # Profile → Account ID (see below)
    ) as client:
        overview = await client.market_overview.list(limit=5)
        for market in overview.markets:
            ticks = market.last_price.ticks if market.last_price is not None else None
            print(market.symbol, ticks)

        open_orders = await client.orders.list_open()
        print(f"{len(open_orders.orders)} open orders")

asyncio.run(main())
```

## Credentials

| Value | Where to find it | Constructor parameter |
| --- | --- | --- |
| API key id | **API** → create or view key | `api_key_id` |
| API private key | Shown once when the key is created | `api_private_key` |
| Account ID | **Profile** → **Account ID** (e.g. `RLxqJGUDg92`) | `default_account_id` |

Pass all credentials as **constructor parameters**. The SDK does not read
environment variables unless you pass them in yourself (or use `from_env()` in
scripts; see below).

`api_private_key` accepts the 64-character hex Ed25519 secret from key creation,
or raw 32-byte key material.

`default_account_id` is the **Account ID** string from your Profile page. Use the
value exactly as shown in the app. Do not use an internal numeric id.

`default_account_id` is optional for public market-data calls. It is required for
account-scoped operations such as private realtime channels, bucket transfers, and
some ledger writes.

Automatic request signing gives concurrent identical calls distinct authentication tuples.
Timestamps can lead the local clock by at most five seconds; larger bursts are backpressured
instead of reusing a signature or drifting outside the API's 10-second freshness window.

## Authentication patterns

**Recommended: explicit parameters**

```python
from polyester import AsyncPolyester

client = AsyncPolyester(
    api_key_id="ak_...",
    api_private_key="...",
    default_account_id="RLxqJGUDg92",
)
```

**If your deployment stores secrets in environment variables**, read them in your
application and pass them to the constructor:

```python
import os

from polyester import AsyncPolyester

client = AsyncPolyester(
    api_key_id=os.environ["POLYESTER_API_KEY_ID"],
    api_private_key=os.environ["POLYESTER_API_PRIVATE_KEY"],
    default_account_id=os.environ["POLYESTER_ACCOUNT_ID"],
)
```

The plain `AsyncPolyester(...)` / `Polyester(...)` constructors never implicitly
read `os.environ`.

**Scripts and local tests only:** `AsyncPolyester.from_env()` and
`Polyester.from_env()` load `POLYESTER_API_KEY_ID`, `POLYESTER_API_PRIVATE_KEY`,
and `POLYESTER_ACCOUNT_ID` from the process environment. This is a convenience
helper, not the primary integration pattern.

## Create and cancel orders

```python
from polyester import AsyncPolyester

async with AsyncPolyester(
    api_key_id="ak_...",
    api_private_key="...",
    default_account_id="RLxqJGUDg92",
    default_sub_account_id="",  # main account; omit subaccount scoping
) as client:
    result = await client.orders.create(
        symbol="BNB-USDT",
        side="buy",
        order_type="limit",
        tif="gtc",
        qty="0.01",
        price="100",
        post_only=True,
        client_order_id="my-bot-001",
    )
    print(result.status, result.order_id)

    await client.orders.cancel(client_order_id="my-bot-001")
```

Client order ids accept 1 to 36 ASCII letters, digits, `.`, `_`, `:`, `/`, and
`-`. Batch create accepts at most 20 orders. Treat a cancel response as an
admission acknowledgement and reconcile with `list_open` before releasing local
state.

Use **decimal strings** or `Decimal` for human-facing `qty` / `price` inputs.
Do **not** pass floats. `ticks` on `Price` means Polyester protocol price units
(fixed 1e6), not market tick-size alignment (server validates tick size).

### For bots (scaled integers)

Stay in integer space; no string round-trip:

```python
from polyester import Price, Quantity

result = await client.orders.create(
    symbol="BNB-USDT",
    side="buy",
    order_type="limit",
    tif="gtc",
    qty=Quantity.from_scaled(1_000_000, scale=8),  # already wire units
    price=Price.from_ticks(100_000_000),            # 100.000000 at 1e6
    post_only=True,
)
# Reads expose the same types: order.price.ticks, order.orig_qty.scaled
```

Compatible values from fills/books can be passed back into writes when the
instrument/domain matches.


Your API key needs a policy that allows trading. Spot orders spend **trading**
balance (see below).

## Triggers

`triggers.list(status=...)` filters by lifecycle status. Valid values:

`created`, `armed`, `running`, `completed`, `cancelled`, `failed`, `paused`

Unknown values raise `ValueError` (they do not silently return an empty list).
Response `status` uses the same labels (British spelling `cancelled`).

`orders.get(..., include_attached_risk=True)` returns policy data on
`order.attached_risk` (take-profit / stop-loss / trailing-stop). `Order` also
exposes `post_only`.

## Balances: funding vs trading

Ledger balances have separate **funding** and **trading** buckets per asset.

- An external deposit can stop in **funding** or continue to **trading**,
  depending on its configured route.
- Spot orders spend **trading** balance.
- Move funds funding → trading in the Polyester UI (**Funding → Unified Trading**)
  or on-chain via the funding wallet.

SDK notes:

- **Funding → trading:** on-chain `TradingGateway.deposit` (not an API-key RPC).
  Either encode calldata or submit a UserOp: pass an owner EOA private key to
  `PolyesterSmartAccount` (SDK derives the Polyester Safe; no UI-exported owner key).
- **Funding → external:** on-chain `FundingAccount.withdrawToChain` (same `polyester.chain`).
- **Funding → another user's funding wallet:** on-chain `FundingAccount.UAssetTransfer`
  via wallet/smart-account signing in the Polyester app (not an API-key RPC).
- **Trading → funding:** `client.trading_withdraws.create_to_funding(...)` with a
  signed intent payload.
- **Trading → trading (another account):** `client.internal_transfers.create(...)`.

```python
from polyester.chain import (
    POLYESTER_TESTNET_ENVIRONMENT,
    PolyesterSmartAccount,
    encode_trading_gateway_deposit,
    encode_funding_withdraw_to_chain,
    encode_withdraw_destination,
    quote_zipper_fee,
)

account = PolyesterSmartAccount(owner_private_key="0x…")  # caller-supplied EOA

# Funding → Trading
deposit = encode_trading_gateway_deposit(
    trading_gateway=POLYESTER_TESTNET_ENVIRONMENT.contracts.trading_gateway_address,
    u_asset_id="0x…",
    quantity_scaled=10**18,  # 1 USDT at 18 decimals
)
account.send_calls([deposit])

# Funding → external (quote Zipper fee first)
fee = quote_zipper_fee(
    chain_id=6,  # BSC testnet Zipper id
    z_token="0x…",
    zipper_endpoint=POLYESTER_TESTNET_ENVIRONMENT.contracts.zipper_endpoint_address,
)
withdraw = encode_funding_withdraw_to_chain(
    funding_account=POLYESTER_TESTNET_ENVIRONMENT.contracts.funding_account_address,
    chain_id=6,
    z_token="0x…",
    withdraw_destination=encode_withdraw_destination(address="0x…", is_case_sensitive=False),
    z_amount=5 * 10**18,
    max_fee=fee.fee + fee.fee // 10,
)
account.send_calls([withdraw])
```

Whitelist toggles / destination entries / GuardRegistry signer setup are also encoded under
`polyester.chain` (`encode_add_allowed_external_destinations`, …).

Pass `default_account_id` (your Profile **Account ID**) on the client for bucket
transfers and other account-scoped ledger operations.

Format u128 wire amounts with the public helper (18-decimal scale):

```python
from polyester import format_ledger_u128

print(format_ledger_u128(balance.funding), format_ledger_u128(balance.trading))
```

## Public market data

Public endpoints do not require an API key. Authenticated endpoints use the
credentials above.

```python
candles = await client.market_data.get_candles(symbol="BTC-USDT", timeframe="1m", limit=50)
current = await client.market_data.get_current_candle(symbol="BTC-USDT", timeframe="1m")
trades = await client.market_data.get_trades(symbol="BTC-USDT", limit=20)

subscription = await client.market_data.subscribe_trades(symbol="BNB-USDT")
subscription.set_on_error(lambda error: print(f"realtime interruption: {error}"))
async with subscription:
    async for trade in subscription:
        print(trade.price.ticks if trade.price else None, trade.qty.scaled if trade.qty else None)
        break
```

Merged market overview stream (snapshot + live updates). The create call waits
for the WebSocket handshake and initial snapshot:

```python
sub = await client.market_overview.create_subscription(
    on_error=lambda error: print(f"managed overview failed: {error}")
)
async with sub:
    async for markets in sub:
        print(len(markets), "rows")
        break
```

### Realtime delivery contract

- Realtime is binary-only. The client negotiates the `centrifuge-protobuf`
  WebSocket subprotocol and consumes protobuf publications from `:proto`
  channels. ConnectRPC's optional JSON wire mode does not apply to realtime.
  Inbound WebSocket messages are capped at 4 MiB.
- Subscription queues are bounded. If the consumer falls behind, the SDK raises
  `PolyesterRealtimeOverflowError` and faults the subscription; it does **not**
  silently drop updates.
- Reconnects use capped exponential backoff with per-subscription jitter.
  `set_on_error(...)` observes background feed interruptions; async iteration
  still raises terminal failures.
- Orderbook sequence gaps trigger a REST snapshot refresh. Use
  `on_sequence_gap` / `on_reconnect` / `on_snapshot_refresh` on
  `orderbook.create_subscription(...)` for recovery observability.
- Managed snapshot-then-stream subscriptions disable transport auto-reconnect
  so they can rebuild REST state between reconnect attempts. Buffered
  publications survive a failed snapshot retry and are merged exactly once
  after recovery; cancellation closes the replacement socket. Managed create
  methods await the handshake and initial snapshot before returning.

## Sync client

The sync `Polyester` client exposes the same service tree and constructor
parameters:

```python
from polyester import Polyester

with Polyester(
    api_key_id="ak_...",
    api_private_key="...",
    default_account_id="RLxqJGUDg92",
) as client:
    balances = client.balances.list()
```

Realtime subscriptions are available via `subscribe_sync` helpers on the sync
client. Private API-key policy snapshots use
`client.policies.subscribe_api_policies_sync(...)` (async:
`await client.policies.subscribe_api_policies(...)`).

## Testing (contributors)

**CI (no network):**

```bash
python -m pytest tests/unit tests/hardening -q
```

**Live selection (A7):** public smoke vs credentialed suites are selectable via markers.

```bash
# Public smoke (read-only smoke; no mutation/funded)
python -m pytest -m "public_smoke" -q

# Credentialed live integration (requires API-key env)
python -m pytest -m "credentialed and not mutation and not funded" -q

# Local L2 hardening only (no network)
python -m pytest tests/hardening -q
```

**Live devnet tests** use a local `.env` file in the test harness only. Fixtures
load values from env and pass them as explicit constructor parameters; the same
pattern application code should use.

```bash
cp .env.example .env
# fill in POLYESTER_API_KEY_ID, POLYESTER_API_PRIVATE_KEY, POLYESTER_ACCOUNT_ID

pip install -e ".[dev]"
python -m pytest tests/unit tests/hardening -q
./scripts/test_all.sh   # optional: unit + live tiers
./scripts/smoke_realtime.sh   # realtime unit + live heartbeat before release
```

Use `python -m pytest` (not bare `pytest`) so tests run in the same venv as `pip install`.
Set `POLYESTER_TEST_MUTATION=1` for state-changing tests. Funded mutations
require both `POLYESTER_TEST_MUTATION=1` and `POLYESTER_TEST_FUNDED=1`. For a
release-certification run, set `POLYESTER_TEST_STRICT_LIVE=1`; any skipped test
then fails instead of making an incomplete live run appear green. Missing or
malformed credentials must fail under strict live (not soft-skip as green). The
session printer scopes executed/skipped/failed counts to `@pytest.mark.integration`
tests and enforces `POLYESTER_TEST_MIN_EXECUTED` (default 5) when credentials are
present.

Legacy stress tests that use non-dry-run `cancel_all` require
`POLYESTER_TEST_ACCOUNT_WIDE_CLEANUP=1`. Set it only for a dedicated test
account; those tests may cancel every open order in their selected symbol.

CI requires every public Connect RPC in gen to be wrapped or listed in
`sdk-coverage.toml`. Contributors: `python scripts/check_sdk_coverage.py`.
CI refreshes `sdk-capabilities.json` and the README capability table on the
same branch when they drift (same-repo PRs / pushes to `main`).

**Pre-release checklist (realtime changes):**

```bash
cd polyester-sdk-python
python -m venv /tmp/polyester-pypi-test && source /tmp/polyester-pypi-test/bin/activate
pip install -e ".[dev]"
python -m pytest tests/unit -q
./scripts/smoke_realtime.sh

cd ../polyester-examples-python
pip install -e "../polyester-sdk-python"
python -m pytest -q
python3 examples/04_public_realtime_trades.py
python3 examples/05_public_orderbook_stream.py
```

Then bump the version, update `CHANGELOG.md`, build, and publish to PyPI. Install from the new wheel (not editable) and rerun `smoke_realtime.sh` once to confirm the published artifact.

## Changelog

See [CHANGELOG.md](CHANGELOG.md).

## Transport

Connect RPC over HTTP via generated clients in `src/polyester/gen/`. Wire format
defaults to **binary protobuf**; pass `wire_format="json"` for debugging.

Some RPCs may return HTTP 404 on devnet. The SDK raises `PolyesterRouteNotFoundError`
with a clearer message than `[unimplemented]: Not Found`.
