From ff20962893143a65ed8036b30a89650a66096026 Mon Sep 17 00:00:00 2001 From: David Irvine Date: Sat, 2 Nov 2024 18:32:26 +0000 Subject: [PATCH] feat(python): add Python bindings for Autonomi This commit introduces Python bindings for the Autonomi crate using PyO3, making the Autonomi network client accessible from Python applications. Key changes: - Add autonomi-py crate with PyO3 bindings - Configure workspace to include Python package - Set up maturin build system for Python package - Add GitHub Actions workflow for building and publishing Python wheels - Configure cross-platform builds for Linux, macOS, and Windows - Add Python 3.8-3.12 support The Python package provides bindings for core Autonomi functionality including: - Network client connection - Data upload/download - Wallet management - Payment handling Build artifacts will be published to PyPI when a new version is tagged. --- .github/workflows/python-publish.yml | 190 ++++++++++ .gitignore | 10 + Cargo.lock | 91 +++++ autonomi/Cargo.toml | 3 + autonomi/README.md | 191 +++++++++- autonomi/examples/autonomi_advanced.py | 79 ++++ autonomi/examples/autonomi_data_registers.py | 89 +++++ autonomi/examples/autonomi_example.py | 38 ++ autonomi/examples/autonomi_private_data.py | 90 +++++ .../examples/autonomi_private_encryption.py | 75 ++++ autonomi/examples/autonomi_vault.py | 53 +++ autonomi/examples/basic.py | 70 ++++ autonomi/pyproject.toml | 34 ++ autonomi/src/lib.rs | 3 + autonomi/src/python.rs | 357 ++++++++++++++++++ 15 files changed, 1372 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/python-publish.yml create mode 100644 autonomi/examples/autonomi_advanced.py create mode 100644 autonomi/examples/autonomi_data_registers.py create mode 100644 autonomi/examples/autonomi_example.py create mode 100644 autonomi/examples/autonomi_private_data.py create mode 100644 autonomi/examples/autonomi_private_encryption.py create mode 100644 autonomi/examples/autonomi_vault.py create mode 100644 autonomi/examples/basic.py create mode 100644 autonomi/pyproject.toml create mode 100644 autonomi/src/python.rs diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml new file mode 100644 index 0000000000..3c19691444 --- /dev/null +++ b/.github/workflows/python-publish.yml @@ -0,0 +1,190 @@ +name: Build and Publish Python Package + +on: + push: + tags: + - 'XXX*' + +permissions: + id-token: write + contents: read + +jobs: + macos: + runs-on: macos-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x86_64, aarch64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + run: | + mkdir -p autonomi/python/autonomi_client + cat > autonomi/python/autonomi_client/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + args: --release --out dist + sccache: 'true' + working-directory: ./autonomi + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.whl + if-no-files-found: error + + windows: + runs-on: windows-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: ${{ matrix.target }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + shell: cmd + run: | + mkdir autonomi\python\autonomi_client + echo from ._autonomi import * > autonomi\python\autonomi_client\__init__.py + echo __version__ = "0.2.33" >> autonomi\python\autonomi_client\__init__.py + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + args: --release --out dist + sccache: 'true' + working-directory: ./autonomi + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.whl + if-no-files-found: error + + linux: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + target: [x86_64] + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + target: x86_64-unknown-linux-gnu + - name: Install dependencies + run: | + python -m pip install --user cffi + python -m pip install --user patchelf + rustup component add rustfmt + - name: Create Python module structure + run: | + mkdir -p autonomi/python/autonomi_client + cat > autonomi/python/autonomi_client/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build wheels + uses: PyO3/maturin-action@v1 + with: + target: ${{ matrix.target }} + manylinux: auto + args: --release --out dist + sccache: 'true' + working-directory: ./autonomi + before-script-linux: | + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y + source $HOME/.cargo/env + rustup component add rustfmt + - name: Upload wheels + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.whl + if-no-files-found: error + + sdist: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + - name: Create Python module structure + run: | + mkdir -p autonomi/python/autonomi_client + cat > autonomi/python/autonomi_client/__init__.py << EOL + from ._autonomi import * + __version__ = "0.2.33" + EOL + - name: Build sdist + uses: PyO3/maturin-action@v1 + with: + command: sdist + args: --out dist + working-directory: ./autonomi + - name: Upload sdist + uses: actions/upload-artifact@v3 + with: + name: wheels + path: autonomi/dist/*.tar.gz + if-no-files-found: error + + release: + name: Release + runs-on: ubuntu-latest + needs: [macos, windows, linux, sdist] + permissions: + id-token: write + contents: read + steps: + - uses: actions/download-artifact@v3 + with: + name: wheels + path: dist + - name: Display structure of downloaded files + run: ls -R dist + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + verbose: true + print-hash: true diff --git a/.gitignore b/.gitignore index 99b9fcf479..bf0d0deed0 100644 --- a/.gitignore +++ b/.gitignore @@ -31,3 +31,13 @@ metrics/prometheus/prometheus.yml *.dot sn_node_manager/.vagrant + +# Python +.venv/ +uv.lock +*.so +*.pyc + +*.pyc +*.swp + diff --git a/Cargo.lock b/Cargo.lock index d6bf9f17fb..bc5a9b1894 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1111,6 +1111,7 @@ dependencies = [ "instant", "js-sys", "libp2p 0.54.1", + "pyo3", "rand 0.8.5", "rmp-serde", "self_encryption", @@ -4043,6 +4044,12 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "heck" version = "0.5.0" @@ -5555,6 +5562,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg 1.3.0", +] + [[package]] name = "merkle-cbt" version = "0.3.2" @@ -7016,6 +7032,69 @@ dependencies = [ "prost 0.9.0", ] +[[package]] +name = "pyo3" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" +dependencies = [ + "heck 0.4.1", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn 2.0.77", +] + [[package]] name = "quick-error" version = "1.2.3" @@ -9113,6 +9192,12 @@ dependencies = [ "xattr", ] +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + [[package]] name = "tempfile" version = "3.12.0" @@ -9898,6 +9983,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "unindent" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" + [[package]] name = "universal-hash" version = "0.5.1" diff --git a/autonomi/Cargo.toml b/autonomi/Cargo.toml index 3bdd14f686..3ac4f23e66 100644 --- a/autonomi/Cargo.toml +++ b/autonomi/Cargo.toml @@ -10,6 +10,7 @@ readme = "README.md" repository = "https://github.com/maidsafe/safe_network" [lib] +name = "autonomi" crate-type = ["cdylib", "rlib"] [features] @@ -22,6 +23,7 @@ local = ["sn_networking/local", "sn_evm/local"] registers = ["data"] loud = [] external-signer = ["sn_evm/external-signer", "data"] +extension-module = ["pyo3/extension-module"] [dependencies] bip39 = "2.0.0" @@ -55,6 +57,7 @@ serde-wasm-bindgen = "0.6.5" sha2 = "0.10.6" blst = "0.3.13" blstrs = "0.7.1" +pyo3 = { version = "0.20", optional = true, features = ["extension-module", "abi3-py38"] } [dev-dependencies] alloy = { version = "0.5.3", default-features = false, features = ["std", "reqwest-rustls-tls", "provider-anvil-node", "sol-types", "json", "signers", "contract", "signer-local", "network"] } diff --git a/autonomi/README.md b/autonomi/README.md index 5b95af38e4..5a638b136e 100644 --- a/autonomi/README.md +++ b/autonomi/README.md @@ -156,4 +156,193 @@ Payment token address: 0x5FbDB2315678afecb367f032d93F642f64180aa3 Chunk payments address: 0x8464135c8F25Da09e49BC8782676a84730C318bC Deployer wallet private key: 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 Genesis wallet balance: (tokens: 20000000000000000000000000, gas: 9998998011366954730202) -``` \ No newline at end of file +``` + +## Python Bindings + +The Autonomi client library provides Python bindings for easy integration with Python applications. + +### Installation + +```bash +pip install autonomi-client +``` + +### Quick Start + +```python +from autonomi_client import Client, Wallet, PaymentOption + +# Initialize wallet with private key +wallet = Wallet("your_private_key_here") +print(f"Wallet address: {wallet.address()}") +print(f"Balance: {wallet.balance()}") + +# Connect to network +client = Client.connect(["/ip4/127.0.0.1/tcp/12000"]) + +# Create payment option +payment = PaymentOption.wallet(wallet) + +# Upload data +data = b"Hello, Safe Network!" +addr = client.data_put(data, payment) +print(f"Data uploaded to: {addr}") + +# Download data +retrieved = client.data_get(addr) +print(f"Retrieved: {retrieved.decode()}") +``` + +### Available Modules + +#### Core Components + +- `Client`: Main interface to the Autonomi network + - `connect(peers: List[str])`: Connect to network nodes + - `data_put(data: bytes, payment: PaymentOption)`: Upload data + - `data_get(addr: str)`: Download data + - `private_data_put(data: bytes, payment: PaymentOption)`: Store private data + - `private_data_get(access: PrivateDataAccess)`: Retrieve private data + - `register_generate_key()`: Generate register key + +- `Wallet`: Ethereum wallet management + - `new(private_key: str)`: Create wallet from private key + - `address()`: Get wallet address + - `balance()`: Get current balance + +- `PaymentOption`: Payment configuration + - `wallet(wallet: Wallet)`: Create payment option from wallet + +#### Private Data + +- `PrivateDataAccess`: Handle private data storage + - `from_hex(hex: str)`: Create from hex string + - `to_hex()`: Convert to hex string + - `address()`: Get short reference address + +```python +# Private data example +access = client.private_data_put(secret_data, payment) +print(f"Private data stored at: {access.to_hex()}") +retrieved = client.private_data_get(access) +``` + +#### Registers + +- Register operations for mutable data + - `register_create(value: bytes, name: str, key: RegisterSecretKey, wallet: Wallet)` + - `register_get(address: str)` + - `register_update(register: Register, value: bytes, key: RegisterSecretKey)` + +```python +# Register example +key = client.register_generate_key() +register = client.register_create(b"Initial value", "my_register", key, wallet) +client.register_update(register, b"New value", key) +``` + +#### Vaults + +- `VaultSecretKey`: Manage vault access + - `new()`: Generate new key + - `from_hex(hex: str)`: Create from hex string + - `to_hex()`: Convert to hex string + +- `UserData`: User data management + - `new()`: Create new user data + - `add_file_archive(archive: str)`: Add file archive + - `add_private_file_archive(archive: str)`: Add private archive + - `file_archives()`: List archives + - `private_file_archives()`: List private archives + +```python +# Vault example +vault_key = VaultSecretKey.new() +cost = client.vault_cost(vault_key) +client.write_bytes_to_vault(data, payment, vault_key, content_type=1) +data, content_type = client.fetch_and_decrypt_vault(vault_key) +``` + +#### Utility Functions + +- `encrypt(data: bytes)`: Self-encrypt data +- `hash_to_short_string(input: str)`: Generate short reference + +### Complete Examples + +#### Data Management + +```python +def handle_data_operations(client, payment): + # Upload text + text_data = b"Hello, Safe Network!" + text_addr = client.data_put(text_data, payment) + + # Upload binary data + with open("image.jpg", "rb") as f: + image_data = f.read() + image_addr = client.data_put(image_data, payment) + + # Download and verify + downloaded = client.data_get(text_addr) + assert downloaded == text_data +``` + +#### Private Data and Encryption + +```python +def handle_private_data(client, payment): + # Create and encrypt private data + secret = {"api_key": "secret_key"} + data = json.dumps(secret).encode() + + # Store privately + access = client.private_data_put(data, payment) + print(f"Access token: {access.to_hex()}") + + # Retrieve + retrieved = client.private_data_get(access) + secret = json.loads(retrieved.decode()) +``` + +#### Vault Management + +```python +def handle_vault(client, payment): + # Create vault + vault_key = VaultSecretKey.new() + + # Store user data + user_data = UserData() + user_data.add_file_archive("archive_address") + + # Save to vault + cost = client.put_user_data_to_vault(vault_key, payment, user_data) + + # Retrieve + retrieved = client.get_user_data_from_vault(vault_key) + archives = retrieved.file_archives() +``` + +### Error Handling + +All operations can raise exceptions. It's recommended to use try-except blocks: + +```python +try: + client = Client.connect(peers) + # ... operations ... +except Exception as e: + print(f"Error: {e}") +``` + +### Best Practices + +1. Always keep private keys secure +2. Use error handling for all network operations +3. Clean up resources when done +4. Monitor wallet balance for payments +5. Use appropriate content types for vault storage + +For more examples, see the `examples/` directory in the repository. diff --git a/autonomi/examples/autonomi_advanced.py b/autonomi/examples/autonomi_advanced.py new file mode 100644 index 0000000000..310766192e --- /dev/null +++ b/autonomi/examples/autonomi_advanced.py @@ -0,0 +1,79 @@ +from autonomi_client import Client, Wallet, PaymentOption +import sys + +def init_wallet(private_key: str) -> Wallet: + try: + wallet = Wallet(private_key) + print(f"Initialized wallet with address: {wallet.address()}") + + balance = wallet.balance() + print(f"Wallet balance: {balance}") + + return wallet + except Exception as e: + print(f"Failed to initialize wallet: {e}") + sys.exit(1) + +def connect_to_network(peers: list[str]) -> Client: + try: + client = Client.connect(peers) + print("Successfully connected to network") + return client + except Exception as e: + print(f"Failed to connect to network: {e}") + sys.exit(1) + +def upload_data(client: Client, data: bytes, payment: PaymentOption) -> str: + try: + addr = client.data_put(data, payment) + print(f"Successfully uploaded data to: {addr}") + return addr + except Exception as e: + print(f"Failed to upload data: {e}") + sys.exit(1) + +def download_data(client: Client, addr: str) -> bytes: + try: + data = client.data_get(addr) + print(f"Successfully downloaded {len(data)} bytes") + return data + except Exception as e: + print(f"Failed to download data: {e}") + sys.exit(1) + +def main(): + # Configuration + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + # Initialize + wallet = init_wallet(private_key) + client = connect_to_network(peers) + payment = PaymentOption.wallet(wallet) + + # Upload test data + test_data = b"Hello, Safe Network!" + addr = upload_data(client, test_data, payment) + + # Download and verify + downloaded = download_data(client, addr) + assert downloaded == test_data, "Data verification failed!" + print("Data verification successful!") + + # Example file handling + try: + with open("example.txt", "rb") as f: + file_data = f.read() + file_addr = upload_data(client, file_data, payment) + + # Download and save to new file + downloaded = download_data(client, file_addr) + with open("example_downloaded.txt", "wb") as f_out: + f_out.write(downloaded) + print("File operations completed successfully!") + except IOError as e: + print(f"File operation failed: {e}") + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/autonomi/examples/autonomi_data_registers.py b/autonomi/examples/autonomi_data_registers.py new file mode 100644 index 0000000000..a7b8ba42ff --- /dev/null +++ b/autonomi/examples/autonomi_data_registers.py @@ -0,0 +1,89 @@ +from autonomi_client import Client, Wallet, PaymentOption, RegisterSecretKey +import hashlib + +def handle_data_operations(client: Client, payment: PaymentOption): + """Example of various data operations""" + print("\n=== Data Operations ===") + + # Upload some text data + text_data = b"Hello, Safe Network!" + text_addr = client.data_put(text_data, payment) + print(f"Text data uploaded to: {text_addr}") + + # Upload binary data (like an image) + with open("example.jpg", "rb") as f: + image_data = f.read() + image_addr = client.data_put(image_data, payment) + print(f"Image uploaded to: {image_addr}") + + # Download and verify data + downloaded_text = client.data_get(text_addr) + assert downloaded_text == text_data, "Text data verification failed!" + print("Text data verified successfully") + + # Download and save image + downloaded_image = client.data_get(image_addr) + with open("downloaded_example.jpg", "wb") as f: + f.write(downloaded_image) + print("Image downloaded successfully") + +def handle_register_operations(client: Client, wallet: Wallet): + """Example of register operations""" + print("\n=== Register Operations ===") + + # Create a register key + register_key = client.register_generate_key() + print(f"Generated register key") + + # Create a register with initial value + register_name = "my_first_register" + initial_value = b"Initial register value" + register = client.register_create( + initial_value, + register_name, + register_key, + wallet + ) + print(f"Created register at: {register.address()}") + + # Read current value + values = register.values() + print(f"Current register values: {[v.decode() for v in values]}") + + # Update register value + new_value = b"Updated register value" + client.register_update(register, new_value, register_key) + print("Register updated") + + # Read updated value + updated_register = client.register_get(register.address()) + updated_values = updated_register.values() + print(f"Updated register values: {[v.decode() for v in updated_values]}") + +def main(): + # Initialize wallet and client + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + # Setup + wallet = Wallet(private_key) + print(f"Wallet address: {wallet.address()}") + print(f"Wallet balance: {wallet.balance()}") + + client = Client.connect(peers) + payment = PaymentOption.wallet(wallet) + + # Run examples + handle_data_operations(client, payment) + handle_register_operations(client, wallet) + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("\nAll operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/autonomi_example.py b/autonomi/examples/autonomi_example.py new file mode 100644 index 0000000000..496446173c --- /dev/null +++ b/autonomi/examples/autonomi_example.py @@ -0,0 +1,38 @@ +from autonomi_client import Client, Wallet, PaymentOption + +def main(): + # Initialize a wallet with a private key + # This should be a valid Ethereum private key (64 hex chars without '0x' prefix) + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + wallet = Wallet(private_key) + print(f"Wallet address: {wallet.address()}") + print(f"Wallet balance: {wallet.balance()}") + + # Connect to the network + # These should be valid multiaddresses of network nodes + peers = [ + "/ip4/127.0.0.1/tcp/12000", + "/ip4/127.0.0.1/tcp/12001" + ] + client = Client.connect(peers) + + # Create payment option using the wallet + payment = PaymentOption.wallet(wallet) + + # Upload some data + data = b"Hello, Safe Network!" + addr = client.data_put(data, payment) + print(f"Data uploaded to address: {addr}") + + # Download the data back + downloaded = client.data_get(addr) + print(f"Downloaded data: {downloaded.decode()}") + + # You can also upload files + with open("example.txt", "rb") as f: + file_data = f.read() + file_addr = client.data_put(file_data, payment) + print(f"File uploaded to address: {file_addr}") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/autonomi/examples/autonomi_private_data.py b/autonomi/examples/autonomi_private_data.py new file mode 100644 index 0000000000..3b0d9327e4 --- /dev/null +++ b/autonomi/examples/autonomi_private_data.py @@ -0,0 +1,90 @@ +from autonomi_client import Client, Wallet, PaymentOption, RegisterSecretKey, RegisterPermissions +from typing import List, Optional +import json + +class DataManager: + def __init__(self, client: Client, wallet: Wallet): + self.client = client + self.wallet = wallet + self.payment = PaymentOption.wallet(wallet) + + def store_private_data(self, data: bytes) -> str: + """Store data privately and return its address""" + addr = self.client.private_data_put(data, self.payment) + return addr + + def retrieve_private_data(self, addr: str) -> bytes: + """Retrieve privately stored data""" + return self.client.private_data_get(addr) + + def create_shared_register(self, name: str, initial_value: bytes, + allowed_writers: List[str]) -> str: + """Create a register that multiple users can write to""" + register_key = self.client.register_generate_key() + + # Create permissions for all writers + permissions = RegisterPermissions.new_with(allowed_writers) + + register = self.client.register_create_with_permissions( + initial_value, + name, + register_key, + permissions, + self.wallet + ) + + return register.address() + +def main(): + # Initialize + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + wallet = Wallet(private_key) + client = Client.connect(peers) + manager = DataManager(client, wallet) + + # Store private data + user_data = { + "username": "alice", + "preferences": { + "theme": "dark", + "notifications": True + } + } + private_data = json.dumps(user_data).encode() + private_addr = manager.store_private_data(private_data) + print(f"Stored private data at: {private_addr}") + + # Retrieve and verify private data + retrieved_data = manager.retrieve_private_data(private_addr) + retrieved_json = json.loads(retrieved_data.decode()) + print(f"Retrieved data: {retrieved_json}") + + # Create shared register + allowed_writers = [ + wallet.address(), # self + "0x1234567890abcdef1234567890abcdef12345678" # another user + ] + register_addr = manager.create_shared_register( + "shared_config", + b"initial shared data", + allowed_writers + ) + print(f"Created shared register at: {register_addr}") + + # Verify register + register = client.register_get(register_addr) + values = register.values() + print(f"Register values: {[v.decode() for v in values]}") + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("All operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/autonomi_private_encryption.py b/autonomi/examples/autonomi_private_encryption.py new file mode 100644 index 0000000000..7f71a6b8d6 --- /dev/null +++ b/autonomi/examples/autonomi_private_encryption.py @@ -0,0 +1,75 @@ +from autonomi_client import ( + Client, Wallet, PaymentOption, PrivateDataAccess, + encrypt, hash_to_short_string +) +import json + +def demonstrate_private_data(client: Client, payment: PaymentOption): + """Show private data handling""" + print("\n=== Private Data Operations ===") + + # Create some private data + secret_data = { + "password": "very_secret", + "api_key": "super_secret_key" + } + data_bytes = json.dumps(secret_data).encode() + + # Store it privately + access = client.private_data_put(data_bytes, payment) + print(f"Stored private data, access token: {access.to_hex()}") + print(f"Short reference: {access.address()}") + + # Retrieve it + retrieved_bytes = client.private_data_get(access) + retrieved_data = json.loads(retrieved_bytes.decode()) + print(f"Retrieved private data: {retrieved_data}") + + return access.to_hex() + +def demonstrate_encryption(): + """Show self-encryption functionality""" + print("\n=== Self-Encryption Operations ===") + + # Create test data + test_data = b"This is some test data for encryption" + + # Encrypt it + data_map, chunks = encrypt(test_data) + print(f"Original data size: {len(test_data)} bytes") + print(f"Data map size: {len(data_map)} bytes") + print(f"Number of chunks: {len(chunks)}") + print(f"Total chunks size: {sum(len(c) for c in chunks)} bytes") + +def main(): + # Initialize + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + # Setup + wallet = Wallet(private_key) + print(f"Wallet address: {wallet.address()}") + print(f"Wallet balance: {wallet.balance()}") + + client = Client.connect(peers) + payment = PaymentOption.wallet(wallet) + + # Run demonstrations + access_token = demonstrate_private_data(client, payment) + demonstrate_encryption() + + # Show utility function + print("\n=== Utility Functions ===") + short_hash = hash_to_short_string(access_token) + print(f"Short hash of access token: {short_hash}") + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("\nAll operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/autonomi_vault.py b/autonomi/examples/autonomi_vault.py new file mode 100644 index 0000000000..6a26d3707a --- /dev/null +++ b/autonomi/examples/autonomi_vault.py @@ -0,0 +1,53 @@ +from autonomi_client import Client, Wallet, PaymentOption, VaultSecretKey, UserData + +def main(): + # Initialize + private_key = "1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" + peers = ["/ip4/127.0.0.1/tcp/12000"] + + try: + # Setup + wallet = Wallet(private_key) + client = Client.connect(peers) + payment = PaymentOption.wallet(wallet) + + # Create vault key + vault_key = VaultSecretKey.new() + print(f"Created vault key: {vault_key.to_hex()}") + + # Get vault cost + cost = client.vault_cost(vault_key) + print(f"Vault cost: {cost}") + + # Create user data + user_data = UserData() + + # Store some data in vault + data = b"Hello from vault!" + content_type = 1 # Custom content type + cost = client.write_bytes_to_vault(data, payment, vault_key, content_type) + print(f"Wrote data to vault, cost: {cost}") + + # Read data back + retrieved_data, retrieved_type = client.fetch_and_decrypt_vault(vault_key) + print(f"Retrieved data: {retrieved_data.decode()}") + print(f"Content type: {retrieved_type}") + + # Store user data + cost = client.put_user_data_to_vault(vault_key, payment, user_data) + print(f"Stored user data, cost: {cost}") + + # Get user data + retrieved_user_data = client.get_user_data_from_vault(vault_key) + print("File archives:", retrieved_user_data.file_archives()) + print("Private file archives:", retrieved_user_data.private_file_archives()) + + except Exception as e: + print(f"Error: {e}") + return 1 + + print("All vault operations completed successfully!") + return 0 + +if __name__ == "__main__": + exit(main()) \ No newline at end of file diff --git a/autonomi/examples/basic.py b/autonomi/examples/basic.py new file mode 100644 index 0000000000..b7d8f21619 --- /dev/null +++ b/autonomi/examples/basic.py @@ -0,0 +1,70 @@ +from autonomi_client import Client, Wallet, RegisterSecretKey, VaultSecretKey, UserData + +def external_signer_example(client: Client, data: bytes): + # Get quotes for storing data + quotes, payments, free_chunks = client.get_quotes_for_data(data) + print(f"Got {len(quotes)} quotes for storing data") + print(f"Need to make {len(payments)} payments") + print(f"{len(free_chunks)} chunks are free") + + # Get raw quotes for specific addresses + addr = "0123456789abcdef" # Example address + quotes, payments, free = client.get_quotes_for_content_addresses([addr]) + print(f"Got quotes for address {addr}") + +def main(): + # Connect to network + client = Client(["/ip4/127.0.0.1/tcp/12000"]) + + # Create wallet + wallet = Wallet() + print(f"Wallet address: {wallet.address()}") + + # Upload public data + data = b"Hello World!" + addr = client.data_put(data, wallet) + print(f"Uploaded public data to: {addr}") + retrieved = client.data_get(addr) + print(f"Retrieved public data: {retrieved}") + + # Upload private data + private_access = client.private_data_put(b"Secret message", wallet) + print(f"Private data access: {private_access}") + private_data = client.private_data_get(private_access) + print(f"Retrieved private data: {private_data}") + + # Create register + reg_addr = client.register_create(b"Initial value", "my_register", wallet) + print(f"Created register at: {reg_addr}") + reg_values = client.register_get(reg_addr) + print(f"Register values: {reg_values}") + + # Upload file/directory + file_addr = client.file_upload("./test_data", wallet) + print(f"Uploaded files to: {file_addr}") + client.file_download(file_addr, "./downloaded_data") + print("Downloaded files") + + # Vault operations + vault_key = VaultSecretKey.generate() + vault_cost = client.vault_cost(vault_key) + print(f"Vault creation cost: {vault_cost}") + + user_data = UserData() + cost = client.put_user_data_to_vault(vault_key, wallet, user_data) + print(f"Stored user data, cost: {cost}") + + retrieved_data = client.get_user_data_from_vault(vault_key) + print(f"Retrieved user data: {retrieved_data}") + + # Private directory operations + private_dir_access = client.private_dir_upload("./test_data", wallet) + print(f"Uploaded private directory, access: {private_dir_access}") + client.private_dir_download(private_dir_access, "./downloaded_private") + print("Downloaded private directory") + + # External signer example + external_signer_example(client, b"Test data") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/autonomi/pyproject.toml b/autonomi/pyproject.toml new file mode 100644 index 0000000000..db4fbc4e22 --- /dev/null +++ b/autonomi/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["maturin>=1.0,<2.0"] +build-backend = "maturin" + +[tool.maturin] +features = ["extension-module"] +python-source = "python" +module-name = "autonomi_client._autonomi" +bindings = "pyo3" +target-dir = "target/wheels" + +[project] +name = "autonomi-client" +dynamic = ["version"] +description = "Autonomi client API" +readme = "README.md" +requires-python = ">=3.8" +license = {text = "GPL-3.0"} +keywords = ["safe", "network", "autonomi"] +authors = [ + {name = "MaidSafe Developers", email = "dev@maidsafe.net"} +] +classifiers = [ + "Programming Language :: Python", + "Programming Language :: Python :: Implementation :: CPython", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Rust", + "Development Status :: 4 - Beta", + "License :: OSI Approved :: GNU General Public License v3 (GPLv3)", +] diff --git a/autonomi/src/lib.rs b/autonomi/src/lib.rs index 2f29d04926..38459bf4c3 100644 --- a/autonomi/src/lib.rs +++ b/autonomi/src/lib.rs @@ -56,3 +56,6 @@ pub use bytes::Bytes; pub use libp2p::Multiaddr; pub use client::Client; + +#[cfg(feature = "extension-module")] +mod python; diff --git a/autonomi/src/python.rs b/autonomi/src/python.rs new file mode 100644 index 0000000000..be8a40b923 --- /dev/null +++ b/autonomi/src/python.rs @@ -0,0 +1,357 @@ +use crate::client::{ + archive::ArchiveAddr, + archive_private::PrivateArchiveAccess, + data_private::PrivateDataAccess, + payment::PaymentOption as RustPaymentOption, + vault::{UserData, VaultSecretKey}, + Client as RustClient, +}; +use crate::{Bytes, Wallet as RustWallet}; +use pyo3::exceptions::PyValueError; +use pyo3::prelude::*; +use sn_evm::EvmNetwork; +use xor_name::XorName; + +#[pyclass(name = "Client")] +pub(crate) struct PyClient { + inner: RustClient, +} + +#[pymethods] +impl PyClient { + #[staticmethod] + fn connect(peers: Vec) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let peers = peers + .into_iter() + .map(|addr| addr.parse()) + .collect::, _>>() + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid multiaddr: {}", e)) + })?; + + let client = rt.block_on(RustClient::connect(&peers)).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to connect: {}", e)) + })?; + + Ok(Self { inner: client }) + } + + fn private_data_put( + &self, + data: Vec, + payment: &PyPaymentOption, + ) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let access = rt + .block_on( + self.inner + .private_data_put(Bytes::from(data), payment.inner.clone()), + ) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!( + "Failed to put private data: {}", + e + )) + })?; + + Ok(PyPrivateDataAccess { inner: access }) + } + + fn private_data_get(&self, access: &PyPrivateDataAccess) -> PyResult> { + let rt = tokio::runtime::Runtime::new().unwrap(); + let data = rt + .block_on(self.inner.private_data_get(access.inner.clone())) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!( + "Failed to get private data: {}", + e + )) + })?; + Ok(data.to_vec()) + } + + fn data_put(&self, data: Vec, payment: &PyPaymentOption) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let addr = rt + .block_on( + self.inner + .data_put(bytes::Bytes::from(data), payment.inner.clone()), + ) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to put data: {}", e)) + })?; + + Ok(crate::client::address::addr_to_str(addr)) + } + + fn data_get(&self, addr: &str) -> PyResult> { + let rt = tokio::runtime::Runtime::new().unwrap(); + let addr = crate::client::address::str_to_addr(addr).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid address: {}", e)) + })?; + + let data = rt.block_on(self.inner.data_get(addr)).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get data: {}", e)) + })?; + + Ok(data.to_vec()) + } + + fn vault_cost(&self, key: &PyVaultSecretKey) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let cost = rt + .block_on(self.inner.vault_cost(&key.inner)) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get vault cost: {}", e)) + })?; + Ok(cost.to_string()) + } + + fn write_bytes_to_vault( + &self, + data: Vec, + payment: &PyPaymentOption, + key: &PyVaultSecretKey, + content_type: u64, + ) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let cost = rt + .block_on(self.inner.write_bytes_to_vault( + bytes::Bytes::from(data), + payment.inner.clone(), + &key.inner, + content_type, + )) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to write to vault: {}", e)) + })?; + Ok(cost.to_string()) + } + + fn fetch_and_decrypt_vault(&self, key: &PyVaultSecretKey) -> PyResult<(Vec, u64)> { + let rt = tokio::runtime::Runtime::new().unwrap(); + let (data, content_type) = rt + .block_on(self.inner.fetch_and_decrypt_vault(&key.inner)) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to fetch vault: {}", e)) + })?; + Ok((data.to_vec(), content_type)) + } + + fn get_user_data_from_vault(&self, key: &PyVaultSecretKey) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let user_data = rt + .block_on(self.inner.get_user_data_from_vault(&key.inner)) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get user data: {}", e)) + })?; + Ok(PyUserData { inner: user_data }) + } + + fn put_user_data_to_vault( + &self, + key: &PyVaultSecretKey, + payment: &PyPaymentOption, + user_data: &PyUserData, + ) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let cost = rt + .block_on(self.inner.put_user_data_to_vault( + &key.inner, + payment.inner.clone(), + user_data.inner.clone(), + )) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to put user data: {}", e)) + })?; + Ok(cost.to_string()) + } +} + +#[pyclass(name = "Wallet")] +pub(crate) struct PyWallet { + inner: RustWallet, +} + +#[pymethods] +impl PyWallet { + #[new] + fn new(private_key: String) -> PyResult { + let wallet = RustWallet::new_from_private_key( + EvmNetwork::ArbitrumOne, // TODO: Make this configurable + &private_key, + ) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Invalid private key: {}", e)) + })?; + + Ok(Self { inner: wallet }) + } + + fn address(&self) -> String { + format!("{:?}", self.inner.address()) + } + + fn balance(&self) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let balance = rt + .block_on(async { self.inner.balance_of_tokens().await }) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get balance: {}", e)) + })?; + + Ok(balance.to_string()) + } + + fn balance_of_gas(&self) -> PyResult { + let rt = tokio::runtime::Runtime::new().unwrap(); + let balance = rt + .block_on(async { self.inner.balance_of_gas_tokens().await }) + .map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Failed to get balance: {}", e)) + })?; + + Ok(balance.to_string()) + } +} + +#[pyclass(name = "PaymentOption")] +pub(crate) struct PyPaymentOption { + inner: RustPaymentOption, +} + +#[pymethods] +impl PyPaymentOption { + #[staticmethod] + fn wallet(wallet: &PyWallet) -> Self { + Self { + inner: RustPaymentOption::Wallet(wallet.inner.clone()), + } + } +} + +#[pyclass(name = "VaultSecretKey")] +pub(crate) struct PyVaultSecretKey { + inner: VaultSecretKey, +} + +#[pymethods] +impl PyVaultSecretKey { + #[new] + fn new() -> PyResult { + Ok(Self { + inner: VaultSecretKey::random(), + }) + } + + #[staticmethod] + fn from_hex(hex_str: &str) -> PyResult { + VaultSecretKey::from_hex(hex_str) + .map(|key| Self { inner: key }) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid hex key: {}", e))) + } + + fn to_hex(&self) -> String { + self.inner.to_hex() + } +} + +#[pyclass(name = "UserData")] +pub(crate) struct PyUserData { + inner: UserData, +} + +#[pymethods] +impl PyUserData { + #[new] + fn new() -> Self { + Self { + inner: UserData::new(), + } + } + + fn add_file_archive(&mut self, archive: &str) -> Option { + let name = XorName::from_content(archive.as_bytes()); + let archive_addr = ArchiveAddr::from_content(&name); + self.inner.add_file_archive(archive_addr) + } + + fn add_private_file_archive(&mut self, archive: &str) -> Option { + let name = XorName::from_content(archive.as_bytes()); + let private_access = match PrivateArchiveAccess::from_hex(&name.to_string()) { + Ok(access) => access, + Err(_e) => return None, + }; + self.inner.add_private_file_archive(private_access) + } + + fn file_archives(&self) -> Vec<(String, String)> { + self.inner + .file_archives + .iter() + .map(|(addr, name)| (format!("{:x}", addr), name.clone())) + .collect() + } + + fn private_file_archives(&self) -> Vec<(String, String)> { + self.inner + .private_file_archives + .iter() + .map(|(addr, name)| (addr.to_hex(), name.clone())) + .collect() + } +} + +#[pyclass(name = "PrivateDataAccess")] +#[derive(Clone)] +pub(crate) struct PyPrivateDataAccess { + inner: PrivateDataAccess, +} + +#[pymethods] +impl PyPrivateDataAccess { + #[staticmethod] + fn from_hex(hex: &str) -> PyResult { + PrivateDataAccess::from_hex(hex) + .map(|access| Self { inner: access }) + .map_err(|e| pyo3::exceptions::PyValueError::new_err(format!("Invalid hex: {}", e))) + } + + fn to_hex(&self) -> String { + self.inner.to_hex() + } + + fn address(&self) -> String { + self.inner.address().to_string() + } +} + +#[pyfunction] +fn encrypt(data: Vec) -> PyResult<(Vec, Vec>)> { + let (data_map, chunks) = self_encryption::encrypt(Bytes::from(data)).map_err(|e| { + pyo3::exceptions::PyValueError::new_err(format!("Encryption failed: {}", e)) + })?; + + let data_map_bytes = rmp_serde::to_vec(&data_map) + .map_err(|e| PyValueError::new_err(format!("Failed to serialize data map: {}", e)))?; + + let chunks_bytes: Vec> = chunks + .into_iter() + .map(|chunk| chunk.content.to_vec()) + .collect(); + + Ok((data_map_bytes, chunks_bytes)) +} + +#[pymodule] +fn _autonomi(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_class::()?; + m.add_function(wrap_pyfunction!(encrypt, m)?)?; + Ok(()) +}