# Danube Python SDK

Official Python SDK for [Danube AI](https://danubeai.com) - access services, tools, skills, and user identity through a clean, Pythonic interface.

## Installation

```bash
pip install danube
```

Or install from source:

```bash
pip install -e .
```

## Quick Start

```python
from danube import DanubeClient

# Initialize with API key (or set DANUBE_API_KEY environment variable)
client = DanubeClient(api_key="dk_...")

# List available services
services = client.services.list(limit=5)
for service in services:
    print(f"{service.name}: {service.tool_count} tools")

# Search for tools
tools = client.tools.search("send email")
for tool in tools:
    print(f"{tool.name}: {tool.description}")

# Execute a tool
result = client.tools.execute(
    tool_name="Gmail - Send Email",
    parameters={
        "to": "user@example.com",
        "subject": "Hello from Danube!",
        "body": "This email was sent using the Danube SDK."
    }
)

if result.success:
    print(f"Success: {result.content}")
else:
    print(f"Error: {result.error}")

# Clean up
client.close()
```

## Usage

### Context Manager (Recommended)

```python
from danube import DanubeClient

with DanubeClient(api_key="dk_...") as client:
    services = client.services.list()
    # Client automatically closes when exiting the context
```

### Async Usage

For better performance in async applications:

```python
import asyncio
from danube import AsyncDanubeClient

async def main():
    async with AsyncDanubeClient(api_key="dk_...") as client:
        # Parallel requests
        services, tools = await asyncio.gather(
            client.services.list(limit=10),
            client.tools.search("weather"),
        )

        # Execute a tool
        result = await client.tools.execute(
            tool_name="Weather - Get Current",
            parameters={"city": "San Francisco"}
        )
        print(result.content)

asyncio.run(main())
```

## API Reference

### Services

```python
# List/search services
services = client.services.list(query="github", limit=10)

# Get a specific service
service = client.services.get("service-uuid")

# Get tools for a service
result = client.services.get_tools("service-uuid")
if result.needs_configuration:
    print(f"Configure at: {result.configuration_url}")
else:
    for tool in result.tools:
        print(tool.name)
```

### Tools

```python
# Search for tools
tools = client.tools.search("send email", service_id="optional-filter")

# Get a specific tool
tool = client.tools.get("tool-uuid")

# Execute by ID (faster)
result = client.tools.execute(tool_id="tool-uuid", parameters={"key": "value"})

# Execute by name (searches first)
result = client.tools.execute(tool_name="Gmail - Send Email", parameters={...})

# Check result
if result.success:
    print(result.content)
    print(f"Took {result.duration_ms}ms")
else:
    print(f"Error: {result.error}")

# Batch execute (up to 10 calls)
results = client.tools.batch_execute([
    {"tool_id": "tool-uuid-1", "parameters": {"city": "San Francisco"}},
    {"tool_id": "tool-uuid-2", "parameters": {"city": "New York"}},
])
for r in results:
    print(f"{r.tool_id}: {'ok' if r.success else r.error}")
```

### Skills

```python
# Search for skills
skills = client.skills.search("pdf processing")

# Get full skill content
skill = client.skills.get(skill_id="skill-uuid")
# or
skill = client.skills.get(skill_name="pdf-processing")

print(f"Instructions:\n{skill.skill_md}")

for script in skill.scripts:
    print(f"Script: {script.name}")
    print(script.content)
```

### Identity

```python
# Get user identity
identity = client.identity.get()
print(f"Name: {identity.name}")
print(f"Email: {identity.email}")

```

### Workflows

```python
# List public workflows
workflows = client.workflows.list(query="email automation", limit=10)

# Get workflow details
workflow = client.workflows.get("workflow-uuid")
for step in workflow.steps:
    print(f"Step {step.step_number}: {step.tool_name}")

# Execute a workflow
execution = client.workflows.execute(
    "workflow-uuid",
    inputs={"query": "test", "recipient": "user@example.com"}
)

# Check execution result
result = client.workflows.get_execution(execution.id)
print(f"Status: {result.status}")
for step in result.step_results:
    print(f"  Step {step.step_number}: {step.status}")

# Create a workflow
workflow = client.workflows.create(
    name="My Pipeline",
    steps=[
        {
            "step_number": 1,
            "tool_id": "tool-uuid",
            "tool_name": "Weather - Get Forecast",
            "description": "Fetch forecast",
            "input_mapping": {"city": "{{inputs.city}}"},
        }
    ],
    visibility="private",
    tags=["weather"],
)

# Update a workflow
updated = client.workflows.update("workflow-uuid", name="Renamed Pipeline")

# Delete a workflow
client.workflows.delete("workflow-uuid")
```

### Sites

```python
# Search agent-friendly site directory
sites = client.sites.search("stripe", category="finance", limit=10)

# Get site by ID
site = client.sites.get("site-uuid")
print(f"Domain: {site.domain}")
print(f"Components: {list(site.components.__dict__.keys())}")

# Get site by domain
site = client.sites.get_by_domain("stripe.com")
if site.components.pricing:
    print(f"Pricing: {site.components.pricing}")
```

### Wallet

```python
# Check balance
balance = client.wallet.get_balance()
print(f"Balance: ${balance.balance_dollars:.2f}")

# Get transaction history
transactions = client.wallet.get_transactions()
for tx in transactions:
    print(f"{tx.type}: {tx.amount_cents}c ({tx.created_at})")
```

### Ratings

```python
# Submit a rating
client.ratings.submit(tool_id="tool-uuid", rating=5, comment="Works great!")

# Get your rating for a tool
my_rating = client.ratings.get_mine("tool-uuid")

# Get aggregate ratings
aggregate = client.ratings.get_tool_ratings("tool-uuid")
print(f"Average: {aggregate.average_rating} ({aggregate.total_ratings} ratings)")
```

### Credentials

```python
# Store a credential for a service
result = client.credentials.store(
    service_id="service-uuid",
    credential_type="bearer",
    credential_value="sk-..."
)
print(f"Stored for: {result.service_name}")
```

### Device Auth

```python
# Initiate device auth flow (for CLI onboarding)
code = client.device_auth.request_code()
print(f"Go to {code.verification_url} and enter: {code.user_code}")

# Poll for token
import time
while True:
    token = client.device_auth.poll_token(code.device_code)
    if token:
        print(f"API key: {token.api_key}")
        break
    time.sleep(code.interval)
```

## Configuration

### Environment Variables

| Variable | Description | Default |
|----------|-------------|---------|
| `DANUBE_API_KEY` | Your Danube API key | (required) |
| `DANUBE_API_URL` | API base URL | `https://api.danubeai.com` |
| `DANUBE_TIMEOUT` | Request timeout (seconds) | `30` |
| `DANUBE_MAX_RETRIES` | Max retry attempts | `3` |

### Programmatic Configuration

```python
from danube import DanubeClient

client = DanubeClient(
    api_key="dk_...",
    base_url="https://api.danubeai.com",
    timeout=60.0,
    max_retries=5,
)
```

## Error Handling

```python
from danube import DanubeClient
from danube.exceptions import (
    NotFoundError,
    ExecutionError,
    ConfigurationRequiredError,
    RateLimitError,
    AuthenticationError,
)

with DanubeClient() as client:
    try:
        result = client.tools.execute(tool_id="invalid-id")
    except AuthenticationError:
        print("Invalid API key")
    except NotFoundError as e:
        print(f"Tool not found: {e}")
    except ExecutionError as e:
        print(f"Execution failed: {e}")
    except ConfigurationRequiredError as e:
        print(f"Configure credentials at: {e.configuration_url}")
    except RateLimitError as e:
        if e.retry_after:
            print(f"Rate limited. Retry in {e.retry_after}s")
```

## Models

### Service

| Field | Type | Description |
|-------|------|-------------|
| `id` | str | Service UUID |
| `name` | str | Service name |
| `description` | str | Service description |
| `service_type` | str | "mcp_server", "api", "internal", or "website" |
| `tool_count` | int | Number of available tools |
| `is_connected` | bool | Whether MCP service is connected |

### Tool

| Field | Type | Description |
|-------|------|-------------|
| `id` | str | Tool UUID |
| `name` | str | Tool name |
| `description` | str | Tool description |
| `service_id` | str | Parent service ID |
| `parameters` | dict | Parameter definitions |

### ToolResult

| Field | Type | Description |
|-------|------|-------------|
| `success` | bool | Whether execution succeeded |
| `result` | Any | Execution result |
| `error` | str | Error message (if failed) |
| `content` | str | Result as text (property) |
| `duration_ms` | float | Execution time |

### Skill / SkillContent

| Field | Type | Description |
|-------|------|-------------|
| `id` | str | Skill UUID |
| `name` | str | Skill name |
| `description` | str | Skill description |
| `skill_md` | str | Main SKILL.md content |
| `scripts` | list | Script files |
| `references` | list | Reference files |
| `assets` | list | Asset files |

### Identity / Contact

| Field | Type | Description |
|-------|------|-------------|
| `profile` | dict | User profile data |
| `key_people` | list | Important contacts |
| `contacts` | list | General contacts |
| `name` | str | Contact name |
| `email` | str | Contact email |

### WorkflowDetail / WorkflowStep

| Field | Type | Description |
|-------|------|-------------|
| `id` | str | Workflow UUID |
| `name` | str | Workflow name |
| `description` | str | Workflow description |
| `steps` | list[WorkflowStep] | Ordered execution steps |
| `visibility` | str | "private" or "public" |
| `tags` | list[str] | Workflow tags |
| `step_number` | int | Step order (on WorkflowStep) |
| `tool_id` | str | Tool UUID (on WorkflowStep) |
| `tool_name` | str | Tool name (on WorkflowStep) |
| `input_mapping` | dict | Template mapping (on WorkflowStep) |

### WorkflowExecution / WorkflowStepResult

| Field | Type | Description |
|-------|------|-------------|
| `id` | str | Execution UUID |
| `status` | str | "pending", "running", "success", or "failed" |
| `inputs` | dict | Input values used |
| `step_results` | list[WorkflowStepResult] | Per-step results |
| `error` | str? | Error message if failed |
| `execution_time_ms` | int? | Total execution time |

### AgentSite / AgentSiteListItem

| Field | Type | Description |
|-------|------|-------------|
| `id` | str | Site UUID |
| `domain` | str | Site domain |
| `url` | str | Full URL |
| `status` | str | "pending", "crawling", "analyzing", "review", "live", "error" |
| `components` | SiteComponents | Extracted structured components |
| `discovered_tools` | list[dict] | Auto-generated tool schemas |
| `category` | str? | Site category |
| `tags` | list[str] | Tags |

### SiteComponents

| Field | Type | Description |
|-------|------|-------------|
| `contact` | dict? | Emails, phones, forms |
| `about` | dict? | Company info |
| `services` | list[dict]? | Services offered |
| `docs` | dict? | Documentation links |
| `pricing` | dict? | Pricing plans |
| `faq` | list[dict]? | FAQ items |
| `legal` | dict? | Privacy policy, ToS |
| `navigation` | list[dict]? | Site navigation |

### WalletBalance / WalletTransaction

| Field | Type | Description |
|-------|------|-------------|
| `balance_cents` | int | Balance in cents |
| `balance_dollars` | float | Balance in dollars |
| `type` | str | Transaction type (on WalletTransaction) |
| `amount_cents` | int | Transaction amount (on WalletTransaction) |

### ToolRating / RatingAggregate

| Field | Type | Description |
|-------|------|-------------|
| `tool_id` | str | Tool UUID |
| `rating` | int | Star rating (1-5) |
| `comment` | str? | Optional comment |
| `average_rating` | float | Average stars (on RatingAggregate) |
| `total_ratings` | int | Total rating count (on RatingAggregate) |

### CredentialStoreResult

| Field | Type | Description |
|-------|------|-------------|
| `success` | bool | Whether storage succeeded |
| `service_id` | str | Service UUID |
| `service_name` | str | Service name |
| `credential_type` | str | "bearer" or "api_key" |

### DeviceCode / DeviceToken

| Field | Type | Description |
|-------|------|-------------|
| `device_code` | str | Device code for polling |
| `user_code` | str | User-facing code (XXXX-XXXX) |
| `verification_url` | str | URL to enter user_code |
| `expires_in` | int | Seconds until expiry |
| `api_key` | str | Returned API key (on DeviceToken) |

## Development

```bash
# Install dev dependencies
pip install -e ".[dev]"

# Run tests
pytest

# Type checking
mypy src/danube

# Format code
black src/danube tests
ruff check src/danube tests
```

## License

MIT License - see [LICENSE](LICENSE) for details.
