Metadata-Version: 2.4
Name: drazill
Version: 0.2.0
Summary: Official alpha Python SDK for the Drazill Prediction Market API
Project-URL: Homepage, https://drazill.com/api
Project-URL: Documentation, https://docs.drazill.com
Project-URL: Repository, https://github.com/IshreetB/drazill
Author: Drazill Inc.
License-Expression: MIT
License-File: LICENSE
Keywords: api,drazill,prediction-market,sdk,trading
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: Typing :: Typed
Requires-Python: >=3.9
Requires-Dist: httpx>=0.25.0
Requires-Dist: pydantic>=2.0.0
Provides-Extra: all
Requires-Dist: websockets>=12.0; extra == 'all'
Provides-Extra: dev
Requires-Dist: mypy>=1.5; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.21; extra == 'dev'
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.1.0; extra == 'dev'
Requires-Dist: websockets>=12.0; extra == 'dev'
Provides-Extra: websocket
Requires-Dist: websockets>=12.0; extra == 'websocket'
Description-Content-Type: text/markdown

# drazill

Official alpha Python SDK for the [Drazill Prediction Market](https://drazill.com) API. The SDK is MIT licensed while the broader monorepo has its own repository license.

> **Status: source-available (this repo) — registry publish pending.** The
> `drazill` package is generated and tested but is **not yet published to
> PyPI**. Install it from source (below) for now; the `pip install drazill`
> commands throughout this README start working once the owner runs the
> release pipeline with registry tokens (see [`../PUBLISHING.md`](../PUBLISHING.md)).

## Installation

Until the package is published, install from the monorepo checkout:

```bash
pip install -e sdks/python
# with WebSocket support:
pip install -e "sdks/python[websocket]"
```

Once published to PyPI (registry publish pending), this becomes:

```bash
pip install drazill
# with WebSocket support:
pip install drazill[websocket]
```

**Requirements:** Python 3.9+

## Quick Start

```python
from drazill import DrazillClient

client = DrazillClient(api_key="<DRAZILL_API_KEY>")

# List active markets
result = client.markets.list_markets(status="ACTIVE", limit=10)
for market in result.items:
    print(f"{market.title} - Volume: ${market.total_volume}")
```

## Runnable quickstart examples

Two self-contained scripts under [`examples/`](examples/) run end-to-end against
**staging** (safe test data, no real money):

| Script | What it does | Scopes |
|---|---|---|
| [`quickstart_markets.py`](examples/quickstart_markets.py) | List open markets → outcomes/prices → live order book → recent trades (read-only) | `markets:read` |
| [`quickstart_trade.py`](examples/quickstart_trade.py) | Fee-inclusive quote → place a small market BUY → print the execution receipt | `markets:read`, `orders:write` (`fees:read` optional) |

```bash
pip install drazill                # or, from the monorepo: pip install -e sdks/python
export DRAZILL_API_KEY="sk_...your staging key..."
export DRAZILL_BASE_URL="https://staging.drazill.com/api/v1"   # optional; this is the default
python examples/quickstart_markets.py
python examples/quickstart_trade.py                            # DRAZILL_QUOTE_ONLY=1 to preview only
```

**Issuing the demo key.** Create a scoped key in the developer portal at
`/developers/api-keys` (the secret is shown once — copy it). Grant only the
scopes the script needs. The trade script spends **test balance**, so fund the
key's account first via the staging "Confirm test deposit" flow or the
test-balance faucet. **Never commit a key** — pass it through the environment.

Environment variables both scripts honor: `DRAZILL_API_KEY` (required),
`DRAZILL_BASE_URL` (defaults to staging). The trade script also honors
`DRAZILL_MARKET_SLUG`, `DRAZILL_OUTCOME_ID`, `DRAZILL_ORDER_QTY` (default `1`
share), and `DRAZILL_QUOTE_ONLY`.

## API coverage & method naming

<!-- generated-surface-stats:start -->
> **Public API surface (generated):** 65 resource groups · 460 operations · 864 types.
> Regenerated from `docs/api/openapi.v1.json` by `sdks/generate_sdks.py`; kept honest by the DX-19 drift gate.
<!-- generated-surface-stats:end -->

The SDK mirrors the **entire public API contract** — every resource
group covering markets, orders, wallet, users, comments, watchlist,
notifications, webhooks, disputes, moderation, rewards, compliance, chat,
copy-trading, lessons, market intelligence, paper trading, sports, crypto,
rank, parlays, conditional/bracket/TWAP/trailing-stop orders, price alerts,
referrals, api-keys, security, geo, fees, and more.

Resources and methods are **generated from the OpenAPI contract**, so each
method name matches the API operation it calls (for example `GET /markets`
becomes `client.markets.list_markets()` and `POST /orders` becomes
`client.orders.place_order(...)`). Path parameters are positional, query
parameters are keyword-only, and request bodies are passed as the first
positional argument (a typed model or a plain `dict`):

```python
client.markets.get_market("market-uuid")                 # path param
client.markets.list_markets(status="ACTIVE", limit=20)    # query params
client.orders.place_order({"market_id": "m", ...})        # request body
```

Every method exists in both sync (`DrazillClient`) and async
(`AsyncDrazillClient`) clients.

> Maintainers: the resource/type modules are produced by
> `sdks/generate_sdks.py` from `docs/api/openapi.v1.json`. Regenerate with
> `python3 scripts/api/generate_openapi.py && python3 sdks/generate_sdks.py`.

## Authentication

Create an API key from your [Drazill dashboard](https://drazill.com/settings/api-keys) (requires a registered account).

```python
# API key authentication (recommended)
client = DrazillClient(api_key="<DRAZILL_API_KEY>")

# Bearer token authentication (for Cognito tokens)
client = DrazillClient(bearer_token="eyJhbGciOi...")
```

### Configuration

```python
client = DrazillClient(
    api_key="<DRAZILL_API_KEY>",
    # Omit to use the default; see "Which environment?" below.
    base_url="https://staging.drazill.com/api/v1",
    timeout=10.0,   # seconds
    max_retries=3,   # retry on 5xx and 429
)
```

### Which environment?

**This SDK talks to staging by default** — test data, test money, no real funds
at risk. That is deliberate: production is not a separate live environment yet,
so the default stays on staging until the release that ships alongside it.

The base URL resolves in this order:

1. the `base_url=` argument, if you pass one;
2. the `DRAZILL_BASE_URL` environment variable;
3. `https://staging.drazill.com/api/v1` (the default).

The realtime URL resolves the same way via `DRAZILL_WS_URL`, defaulting to
`wss://staging.drazill.com/ws`. `client.ws()` derives its URL from whatever the
client resolved, so the two can never disagree.

```bash
export DRAZILL_BASE_URL="http://localhost:8000/api/v1"   # e.g. a local backend
```

You can point at any environment explicitly, but no environment other than
staging is ever selected for you.

### Async Client

```python
from drazill import AsyncDrazillClient

async with AsyncDrazillClient(api_key="<DRAZILL_API_KEY>") as client:
    markets = await client.markets.list_markets(status="ACTIVE")
    print(f"Found {markets.total} markets")
```

## Request signing (optional, FP-107)

If your key was created with `signing_required`, every request must carry an HMAC
signature. Sign `"<unix_ts>.<METHOD>.<path>." + body` with the signing secret
returned once at key creation, and send `X-Drazill-Timestamp` +
`X-Drazill-Signature: v1=<hex>`:

```python
import hashlib
import hmac
import time


def drazill_signature(secret: str, method: str, path: str, body: bytes = b"") -> dict:
    ts = int(time.time())
    signed = f"{ts}.{method.upper()}.{path}.".encode() + body
    digest = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return {"X-Drazill-Timestamp": str(ts), "X-Drazill-Signature": f"v1={digest}"}
```

For safe automated testing, create a key with the `env:paper` scope — real-money
actions are blocked and you target the paper-trading endpoints.

## Markets

```python
# List markets with filters
result = client.markets.list_markets(
    status="ACTIVE",
    q="bitcoin",
    sort_by="total_volume",
    limit=20,
)

# Get a single market
market = client.markets.get_market("market-uuid")
market = client.markets.get_market_by_slug("will-bitcoin-reach-100k")

# Featured and trending
featured = client.markets.get_featured_markets()
trending = client.markets.get_trending_markets()
for_you = client.markets.get_for_you_markets()

# Categories, tags, stats
categories = client.markets.list_categories()
tags = client.markets.get_popular_tags()
stats = client.markets.get_market_stats("market-uuid")

# Trades and price history
trades = client.markets.get_market_trades("market-uuid")
history = client.markets.get_market_price_history("market-uuid", interval="1h")
```

## Orders

```python
# Place a limit order (body may be a dict or a drazill.types model)
order = client.orders.place_order({
    "market_id": "market-uuid",
    "outcome_id": "outcome-uuid",
    "side": "BUY",
    "type": "LIMIT",
    "price": "0.65",
    "quantity": "10",
})

# Place a market order and ask for an execution receipt
order = client.orders.place_order(
    {"market_id": "m", "outcome_id": "o", "side": "BUY", "type": "MARKET", "quantity": "10"},
    include_receipt=True,
)

# List your orders / open orders
result = client.orders.list_my_orders(status="OPEN")
open_orders = client.orders.list_open_orders()

# Cancel
client.orders.cancel_order("order-uuid")
client.orders.cancel_all_orders()

# Order book and price impact
book = client.orders.get_order_book("outcome-uuid")
impact = client.orders.get_price_impact("outcome-uuid", side="BUY", quantity="100")

# Pre-trade quote and your trade history
quote = client.orders.preview_order_quote({"outcome_id": "o", "side": "BUY", "quantity": "10"})
my_trades = client.orders.get_my_trades()
```

## Wallet

```python
wallet = client.wallet.get_wallet()
print(f"Balance: ${wallet.balance} CAD")

portfolio = client.wallet.get_portfolio_summary()
positions = client.wallet.list_positions()
txs = client.wallet.list_transactions(type="TRADE_BUY", limit=50)

# Payments
methods = client.wallet.list_payment_methods()
client.wallet.create_deposit({"amount": "100.00", "payment_method_id": "pm_x"})
client.wallet.create_withdrawal({"amount": "50.00"})

# Analytics
analytics = client.wallet.get_portfolio_analytics()
pnl = client.wallet.get_pnl_today()
```

## Users

```python
me = client.users.get_my_profile()
client.users.update_my_profile({"bio": "Prediction market enthusiast"})
stats = client.users.get_my_stats()

profile = client.users.get_user_profile("username")
leaders = client.users.get_leaderboard()

client.users.follow_user("username")
client.users.unfollow_user("username")
results = client.users.search_users(q="john")

# Avatar upload (multipart) — pass file bytes or an (filename, bytes, content-type) tuple
with open("avatar.png", "rb") as fh:
    client.users.upload_avatar(fh.read())
```

## Comments, Watchlist & Notifications

```python
# Comments
comments = client.comments.get_comments("market-uuid")
client.comments.create_comment({"market_id": "market-uuid", "content": "Interesting!"})
client.comments.toggle_like_comment("comment-uuid")

# Watchlist
items = client.watchlist.get_watchlist()
client.watchlist.add_to_watchlist("market-uuid")
result = client.watchlist.toggle_watchlist("market-uuid")
print(f"Watchlisted: {result.is_watchlisted}")

# Notifications
notifs = client.notifications.get_notifications()
count = client.notifications.get_notification_count()
client.notifications.mark_notifications_read({"notification_ids": ["notification-uuid"]})
client.notifications.mark_all_notifications_read()
```

## Pagination

List endpoints return a typed page object (`items`, `total`, `page`, `pages`);
cursor endpoints (suffixed `_cursor`) return `items` plus `next_cursor`. Rather
than hand-roll the loop, use the exported iterators — pass a bound resource
method and its params and iterate items directly:

```python
from drazill import iter_cursor, iter_offset

# Cursor pagination (the platform's canonical signed-cursor scheme)
for market in iter_cursor(client.markets.list_markets_cursor, limit=50):
    print(market.title)

# Offset pagination
for market in iter_offset(client.markets.list_markets, status="ACTIVE", limit=50):
    print(market.title)
```

Use `aiter_cursor` / `aiter_offset` with `async for` on the async client. Each
iterator fetches one page at a time and stops when the data runs out.

## Webhooks

Create API keys with `webhooks:read` and `webhooks:write` scopes before using the webhook management API.

```python
from drazill import construct_webhook_event, verify_webhook_signature

# Register an endpoint. The whsec_ secret is returned only once.
endpoint = client.webhooks.create_webhook({
    "url": "https://example.com/drazill/webhooks",
    "subscribed_events": ["order.filled", "trade.executed", "wallet.deposit.completed"],
})
print(endpoint.secret)

# Send sandbox deliveries without mutating market/order/wallet/reward state.
client.webhooks.send_webhook_test_event(endpoint.id)
client.webhooks.create_sandbox_webhook_event({
    "endpoint_id": endpoint.id,
    "event_type": "order.filled",
    "data": {"order_id": "00000000-0000-0000-0000-000000000000"},
})

# Inspect and replay failed deliveries.
failures = client.webhooks.list_endpoint_deliveries(endpoint.id, status="FAILED")
for delivery in failures.items:
    client.webhooks.replay_webhook_delivery(delivery.id)

# Verify an incoming request before parsing.
is_valid = verify_webhook_signature(raw_body, headers, endpoint.secret)
if not is_valid:
    raise ValueError("Invalid Drazill webhook signature")

event = construct_webhook_event(raw_body, headers, endpoint.secret)
print(event.type, event.data)
```

## WebSocket (Real-Time Data)

Requires `pip install drazill[websocket]`.

```python
import asyncio
from drazill import DrazillClient

client = DrazillClient(api_key="<DRAZILL_API_KEY>")
ws = client.ws()

async def stream():
    await ws.connect()
    await ws.subscribe("orderbook:OUTCOME_UUID")
    await ws.subscribe("trades:MARKET_UUID")

    async for event in ws.listen():
        event_type = event.get("type")
        channel = event.get("channel", "")
        data = event.get("data", {})
        if event_type == "orderbook_update":
            print(f"Order book update on {channel}: {data}")
        elif event_type == "trade":
            print(f"Trade on {channel}: price={data.get('price')}")

asyncio.run(stream())
```

### Available Channels

| Channel | Description |
|---|---|
| `orderbook:{outcome_id}` | Live order book depth for an outcome |
| `trades:{market_id}` | Trade executions on a market |
| `prices:{outcome_id}` | Price tick updates |
| `market:{market_id}` | Market status changes and resolution |
| `user:{user_id}` | Private notifications and order fills (requires auth) |

## Error Handling

```python
from drazill import (
    ApiError,
    AuthenticationError,
    RateLimitError,
    ValidationError,
    NetworkError,
)

try:
    order = client.orders.place_order({...})
except ValidationError as e:
    print(f"Validation errors: {e.errors}")
except AuthenticationError:
    print("Bad credentials")
except RateLimitError as e:
    print(f"Rate limited. Retry after {e.retry_after}s")
except ApiError as e:
    print(f"API error {e.status}: {e.code} - {e.message}")
except NetworkError as e:
    print(f"Network error: {e.message}")
```

## Type Safety

All responses are Pydantic models with full type hints. Generated request/
response models live under `drazill.types`:

```python
from drazill.types import models

result = client.markets.list_markets(status="ACTIVE")   # typed page object
market: models.MarketListResponse = result.items[0]
print(market.title)  # IDE autocomplete works here
```

## Context Manager

```python
# Sync
with DrazillClient(api_key="<DRAZILL_API_KEY>") as client:
    markets = client.markets.list_markets()

# Async
async with AsyncDrazillClient(api_key="<DRAZILL_API_KEY>") as client:
    markets = await client.markets.list_markets()
```

## License

MIT
