> ## Documentation Index
> Fetch the complete documentation index at: https://noorle.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Python

> Build Noorle plugins with Python using componentize-py

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

## Setup

### Install Tools

```bash theme={null}
# 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

```python theme={null}
# 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

```python theme={null}
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:

```python theme={null}
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`:

```toml theme={null}
[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

```bash theme={null}
# Install dependencies locally
pip install -e .

# Build the plugin
noorle plugin build
```

### Size Considerations

Large dependencies increase plugin size:

```toml theme={null}
# ❌ 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.

```python theme={null}
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`:

```yaml theme={null}
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.

```python theme={null}
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.

```python theme={null}
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`):

```python theme={null}
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:

```yaml theme={null}
permissions:
  network:
    allow:
      - host: "api.example.com"
      - host: "*.github.com"
```

## Error Handling

Return errors gracefully:

```python theme={null}
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

```python theme={null}
# 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:

```bash theme={null}
pytest tests/
```

### Integration Tests

```bash theme={null}
# Build and test the plugin
noorle plugin build
noorle plugin test my-tool --input '{"param": "value"}'
```

## Building and Deploying

### Build

```bash theme={null}
noorle plugin build

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

### Upload

```bash theme={null}
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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
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

```python theme={null}
# 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":**

```bash theme={null}
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

* [Configuration Reference](/build/configuration)
* [Permissions and Security](/build/plugins/permissions-and-security)
* [Publishing Your Plugin](/build/deployment/publishing-and-versioning)
