Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 103 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,17 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- name: Check formatting
run: cargo fmt --all -- --check
- name: Clippy
Expand All @@ -35,13 +42,58 @@ jobs:
--test cli_test --test helpers_test
-- -D warnings

python-clippy:
name: Python clippy
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- name: Clippy agcli-py
run: cargo clippy -p agcli-py --no-deps --features pyo3/extension-module -- -D warnings

sdk-only:
name: SDK-only check
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- name: Clippy sdk-only feature
run: cargo check --no-default-features --features sdk-only

test:
name: Test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- uses: taiki-e/install-action@nextest
- name: Run tests
run: >-
Expand All @@ -60,10 +112,17 @@ jobs:
id-token: write
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
with:
components: llvm-tools-preview
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- uses: taiki-e/install-action@cargo-llvm-cov
- uses: taiki-e/install-action@nextest
- name: Generate coverage
Expand All @@ -88,6 +147,11 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- name: Build release binary
run: cargo build --release
- name: Upload binary
Expand All @@ -103,6 +167,11 @@ jobs:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- uses: actions/setup-python@v5
with:
python-version: "3.12"
Expand All @@ -119,6 +188,39 @@ jobs:
METADATA_CHAIN_ENDPOINT: wss://entrypoint-finney.opentensor.ai:443
- name: Run Python tests
working-directory: python
run: pytest -m "not network"
run: pytest -m "not network and not localnet"
env:
METADATA_CHAIN_ENDPOINT: wss://entrypoint-finney.opentensor.ai:443

wheels:
name: Wheels (${{ matrix.os }} / py${{ matrix.python-version }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: ci-target-cache
workspaces: |
. -> target
python/agcli-py -> target
- uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install maturin
run: pip install "maturin>=1.7,<2.0"
- name: Build wheels
working-directory: python
run: maturin build --release --strip --manifest-path agcli-py/Cargo.toml --out dist
env:
METADATA_CHAIN_ENDPOINT: wss://entrypoint-finney.opentensor.ai:443
- name: Upload wheel artifacts
uses: actions/upload-artifact@v4
with:
name: wheels-${{ matrix.os }}-py${{ matrix.python-version }}
path: python/dist/*.whl
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,11 @@ sdk-only = []
# Enables Docker / live-chain integration test targets (not run by default `cargo test`).
e2e = []

[[bin]]
name = "agcli"
path = "src/main.rs"
required-features = ["cli"]

[[test]]
name = "e2e_test"
path = "tests/e2e_test.rs"
Expand Down
217 changes: 217 additions & 0 deletions docs/tutorials/python-sdk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
# Python SDK tutorial

The Python SDK exposes async and sync clients, wallet lifecycle helpers, typed errors, event subscriptions, localnet helpers, and admin/extrinsic APIs through `agcli`.

## Install

```bash
python -m pip install agcli
```

## Connect + reads (`AsyncClient.connect_network` and `SyncClient`)

Source: `examples/python/connect_and_balance.py`

```python
import asyncio
import os

from agcli import AsyncClient, Config, Network, NetworkError, SyncClient, ValidationError


DEFAULT_ADDRESS = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY"


async def main() -> None:
config = Config.load()
address = os.environ.get("AGCLI_ADDRESS", DEFAULT_ADDRESS)
network = Network.parse(os.environ.get("AGCLI_NETWORK", config.network or "finney"))
endpoint = os.environ.get("AGCLI_WS", config.endpoint or "")

try:
client = await (
AsyncClient.connect(endpoint)
if endpoint
else AsyncClient.connect_network(network)
)
balance = await client.get_balance(address)
metagraph = await client.get_metagraph(1)
portfolio = await client.fetch_portfolio(address)
print(f"async endpoint={client.endpoint}")
print(f"async balance[{address}]={balance.tao:.6f} TAO")
print(f"metagraph netuid={metagraph['netuid']} n={metagraph['n']}")
print(f"portfolio keys={sorted(portfolio.keys())}")
except (NetworkError, ValidationError) as err:
print(f"{err.__class__.__name__}: {err}")
return

sync_client = SyncClient.connect_network(network)
sync_balance = sync_client.get_balance(address)
print(f"sync endpoint={sync_client.endpoint}")
print(f"sync balance[{address}]={sync_balance.tao:.6f} TAO")


if __name__ == "__main__":
asyncio.run(main())
```

## Wallet lifecycle (`Wallet.create`, `Wallet.create_from_uri`, `session(...)`)

Source: `examples/python/wallet_lifecycle.py`

```python
from __future__ import annotations

import tempfile
from pathlib import Path

from agcli import Wallet


def main() -> None:
password = "password123"
with tempfile.TemporaryDirectory(prefix="agcli-wallet-") as wallet_dir:
created = Wallet.create(str(Path(wallet_dir)), "example", password)
print(f"created wallet={created.wallet.name}")
print(f"coldkey mnemonic words={len(created.coldkey_mnemonic.split())}")

uri_wallet = Wallet.create_from_uri(str(Path(wallet_dir)), "//Alice", password)
with uri_wallet.session(password=password) as session_wallet:
print(f"session coldkey unlocked={session_wallet.is_coldkey_unlocked}")
print(f"session hotkey loaded={session_wallet.is_hotkey_loaded}")


if __name__ == "__main__":
main()
```

## Extrinsics (`transfer`, `add_stake`, `set_weights`, `dry_run`, `mev`)

Source: `examples/python/extrinsics_mvp.py`

```python
import asyncio
import os
import tempfile
from pathlib import Path

from agcli import AsyncClient, Wallet


DEFAULT_DEST = "5FHneW46xGXgs5mUiveU4sbTyGBzmst7hQfYQfR6N2P2Vf5Q"


async def main() -> None:
endpoint = os.environ.get("AGCLI_WS", "ws://127.0.0.1:9944")
password = os.environ.get("AGCLI_PASSWORD", "password123")
dest_ss58 = os.environ.get("AGCLI_DEST", DEFAULT_DEST)

with tempfile.TemporaryDirectory(prefix="agcli-py-") as wallet_dir:
wallet = Wallet.create_from_uri(str(Path(wallet_dir)), "//Alice", password)
wallet.load_hotkey("default")
wallet.unlock_coldkey(password)

client = await AsyncClient.connect(endpoint)
transfer_hash = await client.transfer(
wallet,
dest_ss58,
10_000_000,
dry_run=True,
mev=False,
)
stake_hash = await client.add_stake(
wallet,
1,
10_000_000,
dry_run=True,
mev=False,
)
weights_hash = await client.set_weights(
wallet,
1,
[0, 1],
[32_767, 32_768],
0,
dry_run=True,
mev=False,
)
print(f"transfer={transfer_hash}")
print(f"add_stake={stake_hash}")
print(f"set_weights={weights_hash}")


if __name__ == "__main__":
asyncio.run(main())
```

For a minimal transfer-only dry-run script, see `examples/python/transfer_dry_run.py`.

## Events (`subscribe_events`)

Source: `examples/python/subscribe_events.py`

```python
import asyncio
import os

from agcli import AsyncClient
from agcli.events import EventFilter


async def main() -> None:
endpoint = os.environ.get("AGCLI_WS", "ws://127.0.0.1:9944")
limit = int(os.environ.get("AGCLI_EVENT_LIMIT", "5"))
category = os.environ.get("AGCLI_EVENT_CATEGORY", "all")

client = await AsyncClient.connect(endpoint)
stream = client.subscribe_events(EventFilter(category=category))
try:
for _ in range(limit):
event = await stream.__anext__()
print(
f"#{event['block_number']} {event['pallet']}.{event['variant']} "
f"extrinsic_index={event['extrinsic_index']}"
)
finally:
await stream.close()


if __name__ == "__main__":
asyncio.run(main())
```

## Localnet (`start`, `dev_accounts`)

Source: `examples/python/localnet_smoke.py`

```python
import asyncio

from agcli import AsyncClient, localnet


async def main() -> None:
info = await localnet.start()
try:
print(f"localnet endpoint={info.endpoint}")
accounts = localnet.dev_accounts()
for account in accounts:
print(f"{account.name}: {account.ss58}")

client = await AsyncClient.connect(info.endpoint)
alice = accounts[0]
balance = await client.get_balance(alice.ss58)
print(f"alice balance={balance.tao:.6f} TAO")
finally:
localnet.stop(info.container_name)


if __name__ == "__main__":
asyncio.run(main())
```

## Typed errors and config

- Typed exceptions map to the Rust SDK exit code classes (`NetworkError`, `ValidationError`, `AuthError`, `ChainError`, `TimeoutError`, `IOError`).
- `Config.load()` / `Config.save()` let you persist endpoint and wallet defaults in the same shape as the CLI config.
- `examples/python/connect_and_balance.py` shows a typed `try/except` around connection/read calls and config-driven endpoint/network selection.
Loading
Loading