Last Updated: October 4, 2025 Difficulty: Intermediate Time: 2-3 hours for first integration
Want to add a new integration to Piper Morgan? This guide walks you through creating a complete integration from scratch.
What You’ll Build: A weather integration that fetches weather data and exposes it via API
What You’ll Learn:
Before starting, ensure you have:
Service: OpenWeatherMap API
Functionality:
- Fetch current weather
- Get forecast
- Location search
Configuration:
- API key
- Default location
Capabilities:
- routes (API endpoints)
- spatial (location-based data)
cd services/integrations/
mkdir weather
cd weather
touch __init__.py
touch weather_integration_router.py
touch weather_plugin.py
touch config_service.py
mkdir tests
touch tests/test_weather_plugin.py
Your structure should look like:
services/integrations/weather/
├── __init__.py
├── weather_integration_router.py
├── weather_plugin.py
├── config_service.py
└── tests/
└── test_weather_plugin.py
Start with configuration - it’s the foundation.
File: services/integrations/weather/config_service.py
"""Configuration service for Weather integration"""
import os
from typing import Optional
class WeatherConfigService:
"""Manages configuration for Weather integration"""
def __init__(self):
self.api_key = os.getenv("WEATHER_API_KEY", "")
self.default_location = os.getenv("WEATHER_DEFAULT_LOCATION", "San Francisco")
self.api_endpoint = os.getenv(
"WEATHER_API_ENDPOINT",
"https://api.openweathermap.org/data/2.5"
)
def is_configured(self) -> bool:
"""Check if integration is properly configured"""
return bool(self.api_key)
def get_api_key(self) -> Optional[str]:
"""Get API key"""
return self.api_key if self.api_key else None
def get_default_location(self) -> str:
"""Get default location"""
return self.default_location
Key Points:
is_configured() methodNow build your business logic.
File: services/integrations/weather/weather_integration_router.py
"""Weather integration router - business logic"""
from fastapi import APIRouter, HTTPException, Query
from typing import Dict, Any
import httpx
from .config_service import WeatherConfigService
class WeatherIntegrationRouter:
"""Handles weather API integration logic"""
def __init__(self, config_service: WeatherConfigService):
self.config = config_service
self.router = APIRouter(
prefix="/api/integrations/weather",
tags=["weather"]
)
self._setup_routes()
def _setup_routes(self):
"""Define API routes"""
@self.router.get("/current")
async def get_current_weather(
location: str = Query(default=None)
) -> Dict[str, Any]:
"""Get current weather for location"""
if not self.config.is_configured():
raise HTTPException(
status_code=503,
detail="Weather integration not configured"
)
loc = location or self.config.get_default_location()
# Call weather API
async with httpx.AsyncClient() as client:
url = f"{self.config.api_endpoint}/weather"
params = {
"q": loc,
"appid": self.config.get_api_key(),
"units": "metric"
}
response = await client.get(url, params=params)
response.raise_for_status()
return response.json()
@self.router.get("/health")
async def health_check() -> Dict[str, str]:
"""Health check endpoint"""
is_configured = self.config.is_configured()
return {
"status": "ok" if is_configured else "unconfigured",
"service": "weather"
}
Key Points:
@self.router decoratorsWrap your router in the plugin interface.
File: services/integrations/weather/weather_plugin.py
"""Weather integration plugin wrapper"""
from services.plugins.plugin_interface import PiperPlugin, PluginMetadata
from fastapi import APIRouter
from typing import Dict, Any
from .config_service import WeatherConfigService
from .weather_integration_router import WeatherIntegrationRouter
class WeatherPlugin(PiperPlugin):
"""Plugin wrapper for Weather integration"""
def __init__(self):
self.config_service = WeatherConfigService()
self.router_instance = WeatherIntegrationRouter(self.config_service)
def get_metadata(self) -> PluginMetadata:
"""Return plugin metadata"""
return PluginMetadata(
name="weather",
version="1.0.0",
description="Weather data integration",
author="Your Name",
capabilities=["routes", "spatial"]
)
def get_router(self) -> APIRouter:
"""Return FastAPI router"""
return self.router_instance.router
def is_configured(self) -> bool:
"""Check if plugin is configured"""
return self.config_service.is_configured()
async def initialize(self):
"""Initialize plugin"""
# Perform any startup tasks
pass
async def shutdown(self):
"""Cleanup on shutdown"""
# Perform any cleanup
pass
def get_status(self) -> Dict[str, Any]:
"""Return plugin status"""
return {
"configured": self.is_configured(),
"router_prefix": self.router_instance.router.prefix,
"routes": len(self.router_instance.router.routes)
}
# Auto-registration
from services.plugins import get_plugin_registry
_weather_plugin = WeatherPlugin()
get_plugin_registry().register(_weather_plugin)
Key Points:
File: config/PIPER.user.md
Add your plugin to the enabled list:
## 🔌 Plugin Configuration
```yaml
plugins:
enabled:
- github
- slack
- notion
- calendar
- weather # Your new plugin
settings:
weather:
# Plugin-specific settings if needed
Add environment variables to .env:
WEATHER_API_KEY=your_api_key_here
WEATHER_DEFAULT_LOCATION=San Francisco
File: services/integrations/weather/tests/test_weather_plugin.py
"""Tests for Weather plugin"""
import pytest
from services.integrations.weather.weather_plugin import WeatherPlugin
def test_plugin_metadata():
"""Test plugin metadata"""
plugin = WeatherPlugin()
metadata = plugin.get_metadata()
assert metadata.name == "weather"
assert metadata.version == "1.0.0"
assert "routes" in metadata.capabilities
def test_plugin_has_router():
"""Test plugin provides router"""
plugin = WeatherPlugin()
router = plugin.get_router()
assert router is not None
assert router.prefix == "/api/integrations/weather"
@pytest.mark.asyncio
async def test_plugin_lifecycle():
"""Test plugin initialization and shutdown"""
plugin = WeatherPlugin()
# Should not raise errors
await plugin.initialize()
await plugin.shutdown()
def test_plugin_status():
"""Test plugin status reporting"""
plugin = WeatherPlugin()
status = plugin.get_status()
assert "configured" in status
assert "router_prefix" in status
Run tests:
PYTHONPATH=. pytest services/integrations/weather/tests/ -v
Start Piper Morgan:
python3 main.py
Check plugin loaded:
🔌 Initializing Plugin System...
📦 Loaded 5/5 plugin(s)
✅ weather
...
Test endpoints:
# Health check
curl http://localhost:8001/api/integrations/weather/health
# Get weather
curl "http://localhost:8001/api/integrations/weather/current?location=London"
We’ve created a complete example plugin you can reference or copy.
Location: services/integrations/demo/
Files:
config_service.py - Configuration templatedemo_integration_router.py - Router with 3 endpointsdemo_plugin.py - Plugin wrappertests/test_demo_plugin.py - Test suiteTry it:
# Load the demo plugin
python3 main.py
# Visit http://localhost:8001/api/integrations/demo/health
# Run tests
PYTHONPATH=. pytest services/integrations/demo/tests/ -v
What it demonstrates:
How to use it:
services/integrations/demo/ to your new integration nameSee the demo plugin code for detailed comments explaining each part.
import httpx
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
from fastapi import HTTPException
if not self.config.is_configured():
raise HTTPException(
status_code=503,
detail="Integration not configured"
)
def is_configured(self) -> bool:
return all([
self.api_key,
self.endpoint,
# other required config
])
Problem: Plugin doesn’t appear in startup logs
Solutions:
config/PIPER.user.md - is plugin in enabled list?*_plugin.pyProblem: is_configured() returns False
Solutions:
.env file loadedProblem: 404 errors when accessing routes
Solutions:
get_router()Once your integration works:
services/integrations/services/plugins/plugin_interface.pyHappy coding! 🚀