# Python Bindings (pywasot)

Complete guide to using Wasot from Python via the `pywasot` package. For build configuration, see [optional features](./optional_features.md). For [selectors](./selectors.md) and [specificity](./specificity.md) calculation, see the corresponding documentation.

## Installation

### Prerequisites

- Python 3.9+ (tested up to 3.14 with ABI3 compatibility)
- Rust toolchain (for building from source)
- [uv](https://github.com/astral-sh/uv) or pip for package management

### Building from Source

```bash
# Create and activate a virtual environment
uv venv
source .venv/bin/activate

# Install maturin (Rust-Python build tool)
uv tool install maturin

# Build and install the Python bindings
# Note: For Python 3.14+, set ABI3 compatibility flag
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 maturin develop --features python

# Verify installation
python -c "import pywasot; print('pywasot installed successfully')"
```

### Development Workflow

For active development, use `maturin develop` which builds and installs in editable mode:

```bash
# Rebuild after Rust changes
maturin develop --features python

# With release optimizations
maturin develop --features python --release
```

## Quick Start

```python
import pywasot
from pywasot import BlockStartedEvent, TextEvent, BlockFinishedEvent

# Parse Wasot content
content = """.document
.section title="Introduction"
Welcome to Wasot!
"""

for event in pywasot.parse_wasot_string(content):
    if isinstance(event, BlockStartedEvent):
        print(f"Block: {event.meta_line.type_name}")
    elif isinstance(event, TextEvent):
        print(f"Text: {event.lines}")
```

## Core API

### Parsing Functions

#### `parse_wasot(source, *, selectors=None, tools=None, registry=None)`

Universal parser that handles both strings and files:

```python
# Parse string content
for event in pywasot.parse_wasot(".paragraph\nHello world"):
    print(event)

# Parse file (auto-detected by path-like string)
for event in pywasot.parse_wasot("document.wasot"):
    print(event)

# With selectors
for event in pywasot.parse_wasot(content, selectors=["paragraph"]):
    print(event)

# With tools disabled
for event in pywasot.parse_wasot(content, tools=False):
    print(event)
```

#### `parse_wasot_string(content, selectors=None, tools=None)`

Parse Wasot content from a string:

```python
content = """.article
..section
...paragraph class="highlight"
Important text.
"""

for event in pywasot.parse_wasot_string(content):
    if isinstance(event, BlockStartedEvent):
        meta = event.meta_line
        print(f"Block: {meta.type_name} at level {meta.level}")
```

#### `parse_wasot_file(file_path, selectors=None, tools=None)`

Parse from a file:

```python
for event in pywasot.parse_wasot_file("document.wasot"):
    print(event)
```

### Event Types

#### `BlockStartedEvent`

Emitted when a block begins:

```python
event.meta_line.type_name    # Block type (e.g., "paragraph", "section")
event.meta_line.level        # Nesting level (1 = one dot, 2 = two dots, etc.)
event.meta_line.inline_text  # Inline text after ".."
event.meta_line.line_index   # Source line number
event.meta_line.arguments    # List of arguments
event.context                # Shared context for tools
```

#### `TextEvent`

Emitted when text content is encountered:

```python
event.lines  # List of text lines
```

#### `BlockFinishedEvent`

Emitted when a block ends (no additional properties).

### Arguments

Block arguments come in two types:

```python
for arg in event.meta_line.arguments:
    if hasattr(arg, 'key'):
        # Keyword argument: key=value
        print(f"{arg.key}={arg.value}")
    else:
        # Positional argument
        print(f"{arg.value}")
```

## CSS-like Selectors

Filter events using CSS-like selectors:

```python
# Type selector
for event in pywasot.parse_wasot_string(content, selectors=["paragraph"]):
    ...

# ID selector
for event in pywasot.parse_wasot_string(content, selectors=["#intro"]):
    ...

# Class selector
for event in pywasot.parse_wasot_string(content, selectors=[".highlight"]):
    ...

# Attribute selector
for event in pywasot.parse_wasot_string(content, selectors=['[lang="en"]']):
    ...

# Descendant selector
for event in pywasot.parse_wasot_string(content, selectors=["section paragraph"]):
    ...

# Child selector
for event in pywasot.parse_wasot_string(content, selectors=["section > paragraph"]):
    ...

# Multiple selectors
selectors = ["paragraph", ".important", "#main > *"]
for event in pywasot.parse_wasot_string(content, selectors=selectors):
    ...
```

### Specificity Calculation

```python
spec = pywasot.calculate_specificity("section > paragraph.highlight")
print(f"IDs: {spec.ids}")
print(f"Classes/Attributes: {spec.classes_attributes_pseudos}")
print(f"Types: {spec.types}")
```

## Parser Class

For structured event handling, subclass `Parser`:

```python
class MyParser(pywasot.Parser):
    def __init__(self):
        super().__init__(selectors=["paragraph"])
        self.paragraphs = []

    def on_block_started(self, event):
        print(f"Block: {event.meta_line.type_name}")

    def on_text(self, event):
        self.paragraphs.extend(event.lines)

    def on_block_finished(self, event):
        pass

parser = MyParser()
parser.parse_file("document.wasot")
print(parser.paragraphs)
```

## Tools System

[Tools](./tools.md) are callbacks triggered when selectors match during parsing.

### Defining Tools

Use the `@tool` decorator:

```python
from pywasot import tool, parse_wasot_string, BlockStartedEvent

@tool("document")
def document_tool(event):
    """Called when a 'document' block is encountered."""
    if isinstance(event, BlockStartedEvent):
        print(f"Document: {event.meta_line.inline_text}")
        # Store in context for later use
        event.context.put("doc_name", event.meta_line.inline_text)

@tool(["section", "chapter"])
def section_tool(event):
    """Called for both 'section' and 'chapter' blocks."""
    print(f"Section/Chapter started")
```

### Using Tools

```python
# Apply tools during parsing
tools = [document_tool, section_tool]
for event in parse_wasot_string(content, tools=tools):
    print(event)

# Or use apply_tools for an existing iterator
from pywasot import apply_tools
events = parse_wasot_string(content)
for event in apply_tools(events, tools):
    print(event)
```

### Tool Registry

Register tools globally:

```python
from pywasot import register_tool, get_tool_registry

# Register a tool
def my_callback(event):
    print(f"Matched: {event.meta_line.type_name}")

register_tool("my_tool", ["paragraph"], my_callback)

# Or use the registry directly
registry = get_tool_registry()
```

### Context

Tools share a context for passing data:

```python
@tool("page")
def page_tool(event):
    # Global context (persists across all blocks)
    event.context.put("current_page", event.meta_line.inline_text)

    # Scoped context (cleared when block ends)
    event.context.put_scoped("page_local", "some value")

@tool("paragraph")
def paragraph_tool(event):
    # Access values set by other tools
    page = event.context.get("current_page")
    print(f"Paragraph on page: {page}")
```

## JSON-like API

For simpler use cases, use the JSON-like API:

### Loading

```python
import pywasot

# Load from file
data = pywasot.load_file("document.wasot")

# Load from string
data = pywasot.loads(""".article title="Hello"
This is content.
""")

# Access like a dict
print(data['name'])       # "article"
print(data['title'])      # "Hello"
print(data.text)          # "This is content."

# Iterate children
for child in data:
    print(child)
```

### Saving

```python
# Save to file
pywasot.save_file(data, "output.wasot")

# Convert to string
wasot_str = pywasot.dumps(data)
```

### Working with Elements

```python
from pywasot import Block, Text, Document

# Create programmatically
doc = Document()
section = Block(name="section")
section.add_child(Text("Hello world"))
doc.add_child(section)

# Convert to Wasot
print(doc.to_wasot())
```

## Examples

### Extract All Headings

```python
def extract_headings(file_path):
    headings = []
    for event in pywasot.parse_wasot_file(file_path, selectors=["h1", "h2", "h3"]):
        if isinstance(event, BlockStartedEvent):
            headings.append({
                'level': event.meta_line.type_name,
                'text': event.meta_line.inline_text
            })
    return headings
```

### Build Document Tree

```python
class TreeBuilder(pywasot.Parser):
    def __init__(self):
        super().__init__()
        self.stack = []
        self.tree = []

    def on_block_started(self, event):
        node = {
            'type': event.meta_line.type_name,
            'children': [],
            'text': []
        }
        if self.stack:
            self.stack[-1]['children'].append(node)
        else:
            self.tree.append(node)
        self.stack.append(node)

    def on_text(self, event):
        if self.stack:
            self.stack[-1]['text'].extend(event.lines)

    def on_block_finished(self, event):
        if self.stack:
            self.stack.pop()

builder = TreeBuilder()
builder.parse_file("document.wasot")
print(builder.tree)
```

### API Documentation Extractor

```python
@tool('api_endpoint')
def extract_endpoint(event):
    method = None
    path = None
    for arg in event.meta_line.arguments:
        if hasattr(arg, 'key'):
            if str(arg.key) == 'method':
                method = str(arg.value)
            elif str(arg.key) == 'path':
                path = str(arg.value)
    if method and path:
        event.context.put_scoped("endpoint", f"{method} {path}")

@tool('param[required="true"]')
def extract_required_param(event):
    endpoint = event.context.get("endpoint")
    for arg in event.meta_line.arguments:
        if hasattr(arg, 'key') and str(arg.key) == 'name':
            print(f"{endpoint}: required param '{arg.value}'")

tools = [extract_endpoint, extract_required_param]
for event in pywasot.parse_wasot_file("api.wasot", tools=tools):
    pass
```

## Running Examples

The `python/examples/` directory contains runnable examples:

```bash
source .venv/bin/activate
cd python/examples

# Basic parsing
python 01_basic_parsing.py

# Parser class usage
python 02_parser_class.py

# JSON-like API
python 03_json_api.py

# Selectors
python 04_selectors.py
```

## Running Tests

```bash
source .venv/bin/activate
cd python

# Run all tests
python -m pytest tests/

# Run specific test file
python -m pytest tests/unit/test_basic.py

# With verbose output
python -m pytest tests/ -v
```

## Troubleshooting

### PyO3 Version Mismatch

If you see errors about Python version not supported:

```bash
# Use ABI3 forward compatibility
PYO3_USE_ABI3_FORWARD_COMPATIBILITY=1 maturin develop --features python
```

### Import Errors

Ensure you're in the activated virtual environment:

```bash
source .venv/bin/activate
python -c "import pywasot"
```

### Rebuild After Rust Changes

After modifying Rust code, rebuild:

```bash
maturin develop --features python
```

## API Reference

### Functions

| Function | Description |
|----------|-------------|
| `parse_wasot(source, ...)` | Universal parser for strings and files |
| `parse_wasot_string(content, ...)` | Parse Wasot string |
| `parse_wasot_file(path, ...)` | Parse Wasot file |
| `apply_tools(events, tools)` | Apply tools to event iterator |
| `apply_registry_tools(events)` | Apply registered tools |
| `register_tool(name, selectors, callback)` | Register global tool |
| `get_tool_registry()` | Get global tool registry |
| `calculate_specificity(selector)` | Calculate selector specificity |
| `parse_metaline(line, index)` | Parse a single metaline |
| `load(fp)` | Load from file object |
| `loads(s)` | Load from string |
| `dump(obj, fp)` | Write to file object |
| `dumps(obj)` | Convert to string |
| `load_file(path)` | Load from file path |
| `save_file(obj, path)` | Save to file path |

### Classes

| Class | Description |
|-------|-------------|
| `Parser` | Base class for event-driven parsing |
| `BlockStartedEvent` | Event when block starts |
| `TextEvent` | Event with text content |
| `BlockFinishedEvent` | Event when block ends |
| `Context` | Shared context for tools |
| `PyTool` | Tool wrapper |
| `PyToolRegistry` | Tool registry |
| `Block` | Block element (JSON API) |
| `Text` | Text element (JSON API) |
| `Document` | Document root (JSON API) |

### Decorators

| Decorator | Description |
|-----------|-------------|
| `@tool(selectors)` | Mark function as a tool |

## SDK

Layered Archtitecture

- PyO3 Events, incl. Selectors, only iterator
- Wrapped Events into nice types. Iterator and Visitor-Pattern
- Pytdantic like, Beatifoul Soup like 

