# API Plugin - Usage Guide

This document provides practical examples and best practices for using the API Plugin.

## Table of Contents

- [Usage Examples](#usage-examples)
  - [@api Decorator Usage](#api-decorator-usage)
  - [DataCollector - Tables](#datacollector---tables)
  - [DataCollector - Diagrams](#datacollector---diagrams)
  - [DataCollector - Images](#datacollector---images)
  - [DataCollector - Logging](#datacollector---logging)
  - [Batch Processing](#batch-processing)
- [Hook Integration](#hook-integration)
- [CLI Commands](#cli-commands)
- [Best Practices](#best-practices)
- [Integration Examples](#integration-examples)

## Usage Examples

### @api Decorator Usage

Real example from `vipr-core/vipr/plugins/api/controllers.py` (UIController):

```python
from vipr.plugins.api.decorator import api
from vipr.plugins.api.result_storage import ResultStorage
from cement import Controller, ex

@api("/ui/result", "GET",
     response=UIResultResponse,
     tags=["ui"],
     operation_id="getUIResult",
     summary="Get Stored UI Result",
     description="Retrieve a stored UI result by ID (UUID or human-readable)")
@ex(
    help='Retrieve stored UI result by ID',
    arguments=[
        (['--id'], {'help': 'Result ID (UUID or human-readable)', 'required': True}),
    ]
)
def get_result(self):
    """Retrieve stored UI result by ID (UUID or human-readable)."""
    result_id = self.app.pargs.id
    
    # Get result from storage
    storage = ResultStorage()
    result = storage.get_result(result_id)
    
    if result is not None:
        return {
            "result_id": result_id,
            "status": "completed",
            "data": result,
            "retrieved_at": self._get_current_timestamp()
        }
    
    raise Exception(f"UI result {result_id} not found")
```

**Key Features Demonstrated:**
- **Dual Access**: Available via CLI (`vipr ui get-result --id <id>`) and HTTP (`GET /api/ui/result?id=<id>`, service route)
- **Response Model**: Type-safe UIResultResponse for validation
- **Direct Retrieval**: Immediate result access from storage
- **OpenAPI Integration**: Automatic schema generation with tags and descriptions

**Note:** For long-running operations like inference, use manual FastAPI routers with Celery instead of @api decorator. See [README.md](./README.md#integration-example-vipr-inference) for details.

### DataCollector - Tables

```python
# Simple row-by-row construction
app.datacollector.table(
    'parameters',        # id: unique identifier for programmatic access
    'Model Parameters'   # title: human-readable display name
)\
    .add_row(name='SLD', value=2.5, unit='10⁻⁶ Ų⁻²')\
    .add_row(name='thickness', value=100, unit='Å')\
    .add_row(name='roughness', value=5, unit='Å')

# Column-based construction
app.datacollector.table(
    'data',         # id: unique identifier
    'Input Data'    # title: display name
)\
    .add_column('q', [0.01, 0.02, 0.03])\
    .add_column('R', [0.1, 0.05, 0.02])
```

### DataCollector - Diagrams

Diagrams use a two-step process:
1. **Store data**: Use `set_data(name, values)` to store individual data arrays
2. **Define series**: Use `add_series(x, y, error_bars, label)` to define which data arrays to plot together

Each series represents one curve/line in the diagram and establishes the relationship between X and Y data.

```python
# Create diagram with data and series
app.datacollector.diagram('reflectivity', 'Reflectivity Curve')\
    .set_data('q', q_values)\
    .set_data('R_predicted', R_pred)\
    .set_data('R_predicted_error', R_pred_err)\
    .set_data('R_experimental', R_exp)\
    .set_data('R_experimental_error', R_exp_err)\
    .add_series('q', 'R_predicted', error_bars='R_predicted_error', label='Predicted')\
    .add_series('q', 'R_experimental', error_bars='R_experimental_error', label='Experimental')\
    .set_metadata(
        x_label='Q (Ų⁻¹)',
        y_label='Reflectivity',
        y_scale='log',
        x_scale='linear'
    )
```

**Note on CSV extraction:** When diagrams have series definitions, each series is saved as a separate CSV file with aligned X-Y columns and optional error bars. This ensures proper data alignment for each series.

### DataCollector - Images

```python
# From matplotlib figure
import matplotlib.pyplot as plt

fig, ax = plt.subplots()
ax.plot(x, y)
ax.set_xlabel('X')
ax.set_ylabel('Y')

app.datacollector.image('plot', 'My Plot')\
    .set_from_matplotlib(fig, format='png', dpi=150)

# From file
app.datacollector.image('logo', 'Logo')\
    .set_from_file('/path/to/image.png')

# From bytes
with open('image.png', 'rb') as f:
    image_bytes = f.read()

app.datacollector.image('custom', 'Custom Image')\
    .set_from_bytes(image_bytes, format='png')
```

### DataCollector - Logging

**Always use Cement logging** - it's automatically forwarded to DataCollector:

```python
self.app.log.info("Processing started")
self.app.log.warning("Warning message")
self.app.log.error("Error occurred")
```

**Why?** The VIPRLogHandler automatically forwards all Cement logging to DataCollector for frontend timeline display, providing both console output and UI integration without code duplication.

### Batch Processing

```python
# Batch processing example
batch_data = [spectrum1, spectrum2, spectrum3]

# Set batch-level metadata
app.datacollector.set_batch_metadata(
    total_items=len(batch_data),
    processing_mode='batch'
)

# Process each item with dedicated collector (keeps data isolated per item/spectrum)
for i, spectrum in enumerate(batch_data):
    # Create item-specific collector - each item gets its own tables/diagrams/images
    # This prevents data mixing: item 0 has its own 'results' table, item 1 has its own, etc.
    item_dc = app.datacollector.create_item_collector(i)
    
    # Add item-specific data
    item_dc.table('results', f'Results {i}')\
        .add_row(sld=spectrum.sld, thickness=spectrum.thickness)
    
    item_dc.diagram('reflectivity', f'Reflectivity {i}')\
        .set_data('q', spectrum.q)\
        .set_data('R', spectrum.R)\
        .add_series('q', 'R', label=f'Spectrum {i}')

# Add global logs
app.datacollector.add_global_log(
    f"Batch processing complete: {len(batch_data)} items",
    LogLevel.INFO
)
```

## Hook Integration

The API Plugin automatically stores results via the `INFERENCE_COMPLETE_HOOK`:

```python
# In __init__.py
def store_ui_data_directly(app, inference=None):
    """Store UI data after inference completion."""
    # Collect buffered logs
    if hasattr(app.log, '_log_buffer'):
        app.datacollector.data.global_logs.extend(app.log._log_buffer)
        app.log._log_buffer.clear()
    
    # Get result_id from config
    result_id = app.config.get('vipr', 'result_id')
    
    if result_id:
        # Save all collected data
        app.datacollector.save_result(result_id)

app.hook.register('INFERENCE_COMPLETE_HOOK', store_ui_data_directly, weight=200)
```

**Weight 200** ensures storage occurs after all other hooks complete.

## CLI Commands

```bash
# Retrieve stored result
vipr ui get-result --id <id>

# Check if result exists
vipr ui get-result-status --id <id>

# Get storage information
vipr ui get-storage-info

# Cleanup old results (default: 24 hours)
vipr ui cleanup-storage --max-age-hours 24
```

## Best Practices

### 1. Logging

**Always use Cement logging** - it's automatically forwarded to DataCollector:

```python
self.app.log.info("Processing started")
self.app.log.warning("Warning message")
self.app.log.error("Error occurred")
```

### 2. Batch Processing

**Always use item collectors for batch data:**
```python
for i, item in enumerate(batch):
    item_dc = app.datacollector.create_item_collector(i)
    item_dc.table('results').add_row(value=item.value)
```

## Integration Examples

### Backend Integration

Example from `vipr-api/vipr_api/web/routers/inference/tasks.py`:

```python
@router.get("/progress/{task_id}")
async def get_task_progress(task_id: str):
    """Get task progress (status and metadata only)."""
    task_result = AsyncResult(task_id, app=celery_app)
    
    if task_result.state == CeleryState.SUCCESS:
        result_data = task_result.result  # Metadata only (result_id, status, etc.)
        
        return TaskProgressResponse(
            task_id=task_id,
            status=CeleryState.SUCCESS,
            result=result_data  # No UI data - keeps progress polling fast
        )
```

**Note:** Progress endpoint returns only task metadata. UI visualization data (tables, diagrams, images) must be fetched separately via `/api/ui/result?id=<id>` to keep progress polling lightweight and fast.

### Frontend Integration

Example from `vipr-frontend/layers/base/app/stores/inferenceStore.ts`:

```typescript
import type { UIData } from '@vipr/api/index'

export const useInferenceStore = defineStore('inference', () => {
  const displayData = ref<UIData | null>(null);
  
  async function runInference() {
    // 1. Start async inference
    const response = await $inferenceApi.runInferenceAsyncApiInferenceRunPost(config);
    const taskId = response.data?.task_id;
    
    // 2. Poll progress
    const progressTracking = useProgressTracking(taskId);
    await progressTracking.startPolling();
    
    // 3. Fetch final results using task_id as unified identifier
    const resultResponse = await $uiApi.getUIResult(taskId);
    
    if (resultResponse?.data?.data) {
      displayData.value = resultResponse.data.data; // Type: UIData
      // Access: displayData.value.items[0].tables
      //         displayData.value.items[0].diagrams
      //         displayData.value.global_logs
    }
  }
  
  return { displayData, runInference };
});
```

### FastAPI Router Generation

The `build_router()` function in the API Plugin (`vipr-core`) automatically generates FastAPI routers from `@api` decorated methods:

```python
# In vipr-api/vipr_api/main.py
from vipr.plugins.api.fastapi import build_router

# Generate router from all @api decorated methods registered in Cement
auto_generated_router = build_router()
api_router.include_router(auto_generated_router)
```

**Flow:**
1. Scan all loaded Cement controllers for `@api` decorated methods
2. Extract metadata (path, method, request/response models)
3. Generate FastAPI endpoint with proper validation
4. Register with OpenAPI schema

For the Sphinx documentation build, the HTTP route reference is generated separately with `scripts/generate_api_route_docs.py` instead of importing `vipr_api.main`.

## See Also

- **[README.md](./README.md)** - Overview, Architecture, Core Components
- [Inference Plugin](../inference.md) - Uses DataCollector for workflow data
- [Discovery Plugin](../discovery.md) - Auto-generates API endpoints from registry
