Skip to content

Python Guide

Build Noorle plugins with Python.

Why Python?

Strengths

  • Rapid Development - Familiar syntax and extensive standard library
  • Easy JSON Handling - Native support for data processing
  • Dynamic Typing - Flexible for varied data structures
  • Quick Prototyping - Test ideas fast before optimization
  • Clear Syntax - Readable code that's easy to maintain

Best For

  • API integrations
  • Data transformation
  • Quick prototypes
  • JSON/XML processing
  • Script automation ports

Trade-offs

  • Larger binary size than compiled languages
  • Performance overhead vs native code
  • Limited to componentize-py supported features

Quick Start

Prerequisites

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

Create Plugin

bash
# Initialize from template
noorle plugin init my-plugin --template python
cd my-plugin

# Install dependencies (Python, uv, componentize-py, wasm tools)
noorle plugin prepare

# Build and deploy
noorle plugin publish

Minimal Example

python
import wit_world
from wit_world.types import Ok, Err

class WitWorld(wit_world.WitWorld):
    def process(self, input: str) -> wit_world.Result[str, str]:
        if not input:
            return Err("Input cannot be empty")
        return Ok(f"Processed: {input}")

Project Structure

tree
my-plugin/
├── app.py                # Main implementation
├── wit/world.wit         # API definition
├── pyproject.toml        # Python dependencies
├── build.sh              # Build script
├── prepare.sh            # Dependency installer
├── noorle.yaml           # Plugin config (optional)
└── dist/                 # Build output
    ├── plugin.wasm       # Compiled component
    └── *.npack           # Deployment archive

File Descriptions

FilePurpose
app.pyPlugin implementation class
wit/world.witWebAssembly interface definition
pyproject.tomlDependencies (uv/pip format)
build.shCompilation and component creation
prepare.shInstalls Python, uv, and wasm tools

Implementation Guide

Basic Plugin Structure

python
import wit_world
from wit_world.types import Ok, Err

class WitWorld(wit_world.WitWorld):
    """Your plugin implementation"""

    def say_hi(self, name: str) -> str:
        """Simple string function"""
        return f"Hello, {name}!"

    def add(self, a: float, b: float) -> float:
        """Numeric operations"""
        return a + b

    def process(self, input: str) -> wit_world.Result[str, str]:
        """Function with error handling"""
        if not input:
            return Err("Input required")
        return Ok(f"Processed: {input}")

Working with WIT

Define your API in wit/world.wit:

wit
package example:my-plugin;

world my-plugin-component {
    /// Greet a person by name
    export say-hi: func(name: string) -> string;

    /// Add two numbers
    export add: func(a: f64, b: f64) -> f64;

    /// Process with error handling
    export process: func(input: string) -> result<string, string>;
}

Python-specific binding:

python
import wit_world
from wit_world.types import Ok, Err

class WitWorld(wit_world.WitWorld):
    def process(self, input: str) -> wit_world.Result[str, str]:
        # Result maps to WIT result type
        if not input:
            return Err("Input cannot be empty")

        # Process and return Ok
        result = self.do_processing(input)
        return Ok(result)

Error Handling

python
from wit_world.types import Ok, Err
import json

def process(self, data: str) -> wit_world.Result[str, str]:
    """Robust error handling pattern"""
    try:
        # Validate input
        if not data:
            return Err("Data is required")

        if len(data) > 10000:
            return Err(f"Data too large: {len(data)} bytes")

        # Parse and process
        parsed = json.loads(data)

        # Business logic
        result = self.transform_data(parsed)

        return Ok(json.dumps(result))

    except json.JSONDecodeError as e:
        return Err(f"Invalid JSON: {e}")
    except KeyError as e:
        return Err(f"Missing field: {e}")
    except Exception as e:
        return Err(f"Processing failed: {e}")

Environment Variables

python
from wit_world.imports import environment

def get_config(self) -> dict:
    """Read environment configuration"""
    env_vars = environment.get_environment()
    config = {}

    for name, value in env_vars:
        if name == "API_KEY":
            config["api_key"] = value
        elif name == "DEBUG":
            config["debug"] = value.lower() == "true"
        elif name == "TIMEOUT":
            config["timeout"] = int(value)

    return config

JSON Processing

python
import json
from wit_world.types import Ok, Err

def process_json(self, input: str) -> wit_world.Result[str, str]:
    """JSON transformation example"""
    try:
        # Parse input
        data = json.loads(input)

        # Validate structure
        if not isinstance(data, dict):
            return Err("Expected JSON object")

        # Transform data
        data["processed"] = True
        data["plugin"] = "my-plugin"

        # Add nested structures
        if "items" in data:
            data["count"] = len(data["items"])

        # Return formatted JSON
        return Ok(json.dumps(data, indent=2))

    except json.JSONDecodeError as e:
        return Err(f"Invalid JSON: {e}")

HTTP Requests (Optional)

Required WIT imports:

wit
world my-plugin-component {
    import wasi:http/types@0.2.0;
    import wasi:http/outgoing-handler@0.2.0;

    export fetch-data: func(url: string) -> result<string, string>;
}

Implementation:

python
from wit_world.imports import types as http_types
from wit_world.imports import outgoing_handler
from wit_world.imports import poll as poll_module

def fetch_data(self, url: str) -> wit_world.Result[str, str]:
    try:
        # Create headers
        headers = http_types.Fields()
        headers.append("User-Agent", b"my-plugin/1.0")
        headers.append("Accept", b"application/json")

        # Create request
        request = http_types.OutgoingRequest(headers)
        request.set_method(http_types.Method_Get())
        request.set_scheme(http_types.Scheme_Https())
        request.set_authority("api.example.com")
        request.set_path_with_query("/data")

        # Send request
        response = outgoing_handler.handle(request, None)
        response_pollable = response.subscribe()
        poll_module.poll([response_pollable])

        # Get response
        result = response.get()
        if result.is_err():
            return Err("Request failed")

        # Read body
        body = self.read_response_body(result.ok())
        return Ok(body.decode('utf-8'))

    except Exception as e:
        return Err(f"HTTP request failed: {e}")

Required permissions:

yaml
permissions:
  network:
    allow:
      - host: "api.example.com"

Testing

Local Testing with Wasmtime

bash
# Build the plugin
noorle plugin build

# Test with WAVE format
wasmtime run --invoke 'say_hi("World")' dist/plugin.wasm
wasmtime run --invoke 'add(2.5, 3.7)' dist/plugin.wasm
wasmtime run --invoke 'process("test data")' dist/plugin.wasm

# With environment variables
wasmtime run --env API_KEY=test123 --invoke 'process("test")' dist/plugin.wasm

Language-Specific Features

componentize-py Integration

Python code is compiled to WebAssembly using componentize-py:

toml
# pyproject.toml
[project]
dependencies = [
    "componentize-py==0.17.2",  # Required
    # Add your dependencies here
]

Type Hints

python
from typing import List, Dict, Optional
import wit_world
from wit_world.types import Ok, Err

class WitWorld(wit_world.WitWorld):
    def process_list(self, items: List[str]) -> wit_world.Result[str, str]:
        """Type hints improve code clarity"""
        if not items:
            return Err("List cannot be empty")

        processed: List[str] = [item.upper() for item in items]
        return Ok(", ".join(processed))

    def process_dict(self, data: Dict[str, any]) -> wit_world.Result[str, str]:
        """Handle dictionary data"""
        required_fields = ["id", "name"]

        for field in required_fields:
            if field not in data:
                return Err(f"Missing required field: {field}")

        return Ok(json.dumps(data))

Python Built-ins

python
import json
import base64
import hashlib
from datetime import datetime

class WitWorld(wit_world.WitWorld):
    def encode_data(self, data: str) -> str:
        """Use standard library functions"""
        # Base64 encoding
        encoded = base64.b64encode(data.encode()).decode()

        # Generate hash
        hash_obj = hashlib.sha256(data.encode())
        data_hash = hash_obj.hexdigest()

        # Add timestamp
        timestamp = datetime.utcnow().isoformat()

        return json.dumps({
            "encoded": encoded,
            "hash": data_hash,
            "timestamp": timestamp
        })

Build Configuration

Dependencies

pyproject.toml:

toml
[project]
name = "my-plugin"
dependencies = [
    "componentize-py==0.17.2",  # Required for WASM compilation
    # Add your dependencies here
    # Note: Not all Python packages work in WASM
]

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

Build Optimization

The build process automatically optimizes for size:

bash
# Build handled by noorle
noorle plugin build

Build Process

bash
# Build your plugin (handles compilation, optimization, packaging)
noorle plugin build

The build process:

  1. Installs Python dependencies via uv
  2. Compiles Python to WASM with componentize-py
  3. wkg fetches WIT dependencies from imports
  4. Creates component with wasm-tools
  5. Optimizes output
  6. Packages into .npack archive

Troubleshooting

Common Issues

Build Fails

Problem: componentize-py: command not found

Solution:

bash
noorle plugin prepare  # Installs all required tools

Runtime Error

Problem: ModuleNotFoundError: No module named 'wit_world'

Solution:

python
# Ensure class name matches
class WitWorld(wit_world.WitWorld):  # Must be "WitWorld"
    pass

Package Not Supported

Problem: Package X not available in WASM

Solution:

python
# Use alternatives or implement functionality
# Check componentize-py docs for supported packages
# Consider using pure Python implementations

Debugging

Enable debug output:

bash
DEBUG=1 noorle plugin build

Check generated files:

bash
ls -la wit_world/

View WASM exports:

bash
wasm-tools component wit dist/plugin.wasm

Performance

Optimization tips:

  • Keep dependencies minimal
  • Use built-in functions when possible
  • Avoid heavy libraries
  • Pre-compute when possible
  • Cache processed results

Best Practices

Do's

  • ✅ Use Result types for error handling
  • ✅ Validate input early
  • ✅ Use type hints for clarity
  • ✅ Handle all exceptions
  • ✅ Test with wasmtime locally
  • ✅ Keep functions focused

Don'ts

  • ❌ Don't use packages with C extensions
  • ❌ Don't rely on filesystem access
  • ❌ Don't use threading or multiprocessing
  • ❌ Don't ignore error cases
  • ❌ Don't use global mutable state

Security

  • Validate all inputs
  • Use minimal permissions in noorle.yaml
  • Never log sensitive data
  • Sanitize error messages
  • Handle timeouts gracefully

Advanced Topics

Custom WASI Interfaces

wit
// Advanced WIT with custom types
interface types {
    record config {
        endpoint: string,
        timeout: u32,
        retries: u8,
    }

    record response {
        status: u16,
        body: string,
        headers: list<tuple<string, string>>,
    }
}

world my-plugin {
    use types.{config, response};

    export process-with-config: func(cfg: config) -> response;
}
python
from wit_world import types

def process_with_config(self, cfg: types.Config) -> types.Response:
    """Use generated types from WIT"""
    # Process with config
    return types.Response(
        status=200,
        body="Success",
        headers=[("Content-Type", "application/json")]
    )

Performance Monitoring

python
import time

def timed_process(self, input: str) -> wit_world.Result[str, str]:
    """Monitor execution time"""
    start = time.time()

    try:
        result = self.process_internal(input)
        elapsed = time.time() - start

        # Log timing (visible in wasmtime stderr)
        print(f"Processing took: {elapsed:.3f}s", file=sys.stderr)

        return Ok(result)
    except Exception as e:
        return Err(str(e))

Working with Custom Types

python
from dataclasses import dataclass
from typing import List, Optional

@dataclass
class ProcessRequest:
    """Custom request type"""
    id: str
    data: List[str]
    options: Optional[dict] = None

class WitWorld(wit_world.WitWorld):
    def handle_complex(self, input: str) -> wit_world.Result[str, str]:
        """Handle complex custom types"""
        try:
            data = json.loads(input)

            # Parse to custom type
            request = ProcessRequest(
                id=data.get("id"),
                data=data.get("data", []),
                options=data.get("options")
            )

            # Validate
            if not request.id:
                return Err("Request ID required")

            # Process
            result = self.process_request(request)
            return Ok(json.dumps(result))

        except Exception as e:
            return Err(f"Failed to process: {e}")

Resources

Documentation

Tools

Examples

Next Steps

Continue Learning

Build Your Plugin