Skip to main content
Build WebAssembly plugins using Python. Noorle uses componentize-py to compile Python to WASI Preview 2 WebAssembly.

Setup

Install Tools

# Install Noorle CLI
curl -L cli.noorle.dev | sh

# Create a new Python plugin
noorle plugin new my-python-plugin --lang python
cd my-python-plugin

Project Structure

my-python-plugin/
├── src/
│   ├── __init__.py           # Entry point
│   ├── tools.py              # Tool implementations
│   └── utils.py              # Utilities
├── tests/
│   └── test_tools.py
├── pyproject.toml            # Dependencies
├── noorle.yaml               # Plugin config
├── world.wit                 # Component interface
├── .env
└── README.md

Define Tools

Tools are Python functions exported from your plugin.

Simple Function

# src/tools.py
def greet(name: str) -> str:
    """Greet a person by name"""
    return f"Hello, {name}!"

def add(a: float, b: float) -> float:
    """Add two numbers"""
    return a + b

Complex Types

from typing import Dict, List

def process_data(
    items: List[str],
    config: Dict[str, str]
) -> Dict[str, int]:
    """Process a list with configuration"""
    result = {}
    for item in items:
        key = config.get("prefix", "") + item
        result[key] = len(item)
    return result

def search_items(
    query: str,
    limit: int = 10
) -> List[Dict[str, str]]:
    """Search for items"""
    return [
        {"id": str(i), "title": f"{query}_{i}"}
        for i in range(min(limit, 5))
    ]

Type Annotations

Python type hints define the tool interface:
def my_tool(
    required_param: str,          # Required string
    optional_param: str = "default",  # Optional with default
    count: int = 1,               # Integer parameter
    items: List[str] = None,      # List type
    config: Dict[str, str] = None # Dictionary type
) -> str:                         # Return type
    """Tool description"""
    return f"Processed: {required_param}"
Supported types:
  • str - String
  • int - Integer
  • float - Floating point
  • bool - Boolean
  • List[T] - Array of type T
  • Dict[str, T] - Object with string keys
  • Optional[T] - Nullable type

Working with Dependencies

Add Dependencies

Edit pyproject.toml:
[project]
name = "my-python-plugin"
version = "0.1.0"
dependencies = [
    "requests==2.31.0",
    "pandas==2.0.0",
    "pydantic==2.0.0",
]

[build-system]
requires = ["setuptools", "componentize-py"]
build-backend = "backend"

Install and Build

# Install dependencies locally
pip install -e .

# Build the plugin
noorle plugin build

Size Considerations

Large dependencies increase plugin size:
# ❌ Heavy dependencies
numpy = "1.24.0"      # 30MB+ with dependencies
tensorflow = "2.14.0" # 100MB+ with dependencies

# ✅ Lightweight alternatives
statistics               # Standard library stats
decimal                  # High precision math

File I/O

Access allowed filesystem paths via environment or configuration.
import os
from pathlib import Path

def read_config() -> str:
    """Read configuration file"""
    config_path = Path("/allowed/path/config.json")
    return config_path.read_text()

def write_cache(key: str, value: str) -> bool:
    """Write to cache"""
    cache_dir = Path(os.getenv("CACHE_DIR", "/cache"))
    (cache_dir / f"{key}.txt").write_text(value)
    return True

def list_files(directory: str) -> List[str]:
    """List files in directory"""
    dir_path = Path(directory)
    return [f.name for f in dir_path.glob("*")]

Configure Permissions

In noorle.yaml:
permissions:
  filesystem:
    allow:
      - uri: "fs://cache/**"
        access: [read, write]
      - uri: "fs://config/**"
        access: [read]

  environment:
    allow:
      - key: "CACHE_DIR"

Environment Variables

Access configuration via environment variables.
import os

def get_api_credentials() -> Dict[str, str]:
    """Get API credentials from environment"""
    return {
        "api_key": os.getenv("API_KEY", ""),
        "api_secret": os.getenv("API_SECRET", ""),
    }

def get_database_url() -> str:
    """Get database URL with fallback"""
    return os.getenv("DATABASE_URL", "sqlite:///:memory:")
Configure in .env:
API_KEY=sk-1234567890
API_SECRET=secret-value
DATABASE_URL=postgres://localhost/mydb

Network Access

Make HTTP requests to allowed hosts.
import urllib.request
import json

def fetch_data(url: str) -> str:
    """Fetch data from allowed host"""
    try:
        with urllib.request.urlopen(url) as response:
            return response.read().decode('utf-8')
    except Exception as e:
        return f"Error: {str(e)}"

def post_json(url: str, data: Dict) -> str:
    """POST JSON to allowed host"""
    import urllib.parse

    json_data = json.dumps(data).encode('utf-8')
    req = urllib.request.Request(
        url,
        data=json_data,
        headers={"Content-Type": "application/json"}
    )

    with urllib.request.urlopen(req) as response:
        return response.read().decode('utf-8')
Or use requests (add to pyproject.toml):
import requests

def call_api(endpoint: str, params: Dict[str, str]) -> str:
    """Call external API"""
    response = requests.get(
        f"https://api.example.com/{endpoint}",
        params=params,
        timeout=5
    )
    return response.text
Configure network permissions:
permissions:
  network:
    allow:
      - host: "api.example.com"
      - host: "*.github.com"

Error Handling

Return errors gracefully:
def safe_operation(data: str) -> Dict[str, str]:
    """Perform operation with error handling"""
    try:
        if not data:
            return {"error": "Empty input"}

        result = process(data)
        return {"success": True, "result": result}

    except ValueError as e:
        return {"error": f"Validation error: {str(e)}"}
    except Exception as e:
        return {"error": f"Unexpected error: {str(e)}"}

def process(data: str) -> str:
    """Internal function"""
    if not isinstance(data, str):
        raise ValueError("Expected string")
    return data.upper()

Testing

Test your plugin locally before uploading.

Unit Tests

# tests/test_tools.py
import sys
sys.path.insert(0, '../src')

from tools import greet, add, process_data

def test_greet():
    result = greet("Alice")
    assert result == "Hello, Alice!"

def test_add():
    assert add(2, 3) == 5
    assert add(1.5, 2.5) == 4.0

def test_process_data():
    items = ["hello", "world"]
    config = {"prefix": "test_"}
    result = process_data(items, config)
    assert result["test_hello"] == 5
    assert result["test_world"] == 5
Run tests:
pytest tests/

Integration Tests

# Build and test the plugin
noorle plugin build
noorle plugin test my-tool --input '{"param": "value"}'

Building and Deploying

Build

noorle plugin build

# Output:
# ✓ Building plugin...
# ✓ Compiling Python to WebAssembly
# ✓ Creating my-python-plugin.npack (234KB)

Upload

noorle plugin push --api-key YOUR_KEY

# Output:
# ✓ Uploading plugin...
# ✓ Plugin ID: 550e8400-e29b-41d4-a716-446655440000
# ✓ Active version: v1
# ✓ Tools: greet, add, process_data

Best Practices

Input Validation

def validate_input(name: str, age: int) -> Dict[str, str]:
    """Validate and process input"""
    errors = []

    if not name or len(name.strip()) == 0:
        errors.append("Name cannot be empty")

    if age < 0 or age > 150:
        errors.append("Age must be between 0 and 150")

    if errors:
        return {"error": "; ".join(errors)}

    return {"success": True}

Logging

import sys

def log(message: str, level: str = "INFO"):
    """Log message"""
    print(f"[{level}] {message}", file=sys.stderr)

def my_tool(input_data: str) -> str:
    """Tool with logging"""
    log(f"Processing: {input_data}")
    result = process(input_data)
    log(f"Completed", "DEBUG")
    return result

Documentation

def transform_text(
    text: str,
    uppercase: bool = False,
    trim: bool = True
) -> str:
    """
    Transform input text.

    Args:
        text: The text to transform
        uppercase: Convert to uppercase if True
        trim: Remove leading/trailing whitespace if True

    Returns:
        Transformed text
    """
    if trim:
        text = text.strip()
    if uppercase:
        text = text.upper()
    return text

Example: Complete Plugin

# src/tools.py
import os
import json
from typing import Dict, List

def get_weather(city: str, units: str = "celsius") -> Dict[str, str]:
    """
    Get weather for a city.

    Args:
        city: City name
        units: Temperature units (celsius or fahrenheit)

    Returns:
        Weather data
    """
    import urllib.request

    api_key = os.getenv("OPENWEATHER_API_KEY", "")
    if not api_key:
        return {"error": "API key not configured"}

    url = (
        f"https://api.openweathermap.org/data/2.5/weather"
        f"?q={city}&appid={api_key}&units={units}"
    )

    try:
        with urllib.request.urlopen(url) as response:
            data = json.loads(response.read().decode())
            return {
                "city": data.get("name"),
                "temp": str(data["main"]["temp"]),
                "description": data["weather"][0]["description"],
            }
    except Exception as e:
        return {"error": f"Failed to fetch weather: {str(e)}"}

Troubleshooting

Build fails with “componentize-py not found”:
pip install componentize-py
Plugin too large (>5MB):
  • Remove unnecessary dependencies
  • Use lighter alternatives
  • Check import statements
Type errors during build:
  • Ensure all function parameters have type hints
  • Check return type annotations
  • Verify List/Dict syntax: List[str], Dict[str, int]

Next Steps