🌦️ Weather Addon Example (SQLAlchemy Version)¶
This addon showcases how to build a basic weather data API using pAPI, now powered by SQLAlchemy for persistent storage.
With this example, you will learn how to:
- Integrate an SQL database using SQLAlchemy's async engine
- Declare and manage Python dependencies within your addon
- Register and list weather stations
- Retrieve and save real-time weather data from an external API
🗂️ Project Structure¶
my_addons/
└── weather/
├── __init__.py
├── manifest.yaml
├── models.py
├── schemas.py
├── crud.py
└── routers.py
📄 manifest.yaml
¶
Defines the addon metadata and required Python packages:
name: weather
version: 1.0.0
description: Weather data API (SQLAlchemy version)
author: Your Name
python_dependencies:
- "requests>=2.28.0"
🧬 models.py
¶
from datetime import datetime
from sqlalchemy import Column, DateTime, Float, ForeignKey, Integer, String
from sqlalchemy.orm import declarative_base, relationship
Base = declarative_base()
class WeatherStation(Base):
__tablename__ = "weather_stations"
id = Column(Integer, primary_key=True, index=True)
name = Column(String, nullable=False)
latitude = Column(Float, nullable=False)
longitude = Column(Float, nullable=False)
created_at = Column(DateTime, default=datetime.utcnow)
readings = relationship("WeatherReading", back_populates="station")
class WeatherReading(Base):
__tablename__ = "weather_readings"
id = Column(Integer, primary_key=True, index=True)
station_id = Column(Integer, ForeignKey("weather_stations.id"))
temperature = Column(Float)
windspeed = Column(Float)
humidity = Column(Float)
timestamp = Column(DateTime, default=datetime.utcnow)
station = relationship("WeatherStation", back_populates="readings")
📊 schemas.py
¶
from datetime import datetime
from pydantic import BaseModel
class WeatherStationBase(BaseModel):
name: str
latitude: float
longitude: float
class WeatherStationCreate(WeatherStationBase):
pass
class WeatherStationOut(WeatherStationBase):
id: int
created_at: datetime
class Config:
orm_mode = True
class WeatherReadingOut(BaseModel):
id: int
station_id: int
temperature: float | None
windspeed: float | None
humidity: float | None
timestamp: datetime
class Config:
orm_mode = True
🌐 crud.py
¶
import requests
def get_weather(latitude: float, longitude: float) -> dict:
url = (
"https://api.open-meteo.com/v1/forecast"
f"?latitude={latitude}&longitude={longitude}"
"¤t=temperature_2m,wind_speed_10m,relative_humidity_2m"
)
try:
response = requests.get(url, timeout=5)
response.raise_for_status()
current = response.json().get("current")
if not current:
raise ValueError("Missing 'current' field in API response")
return {
"temperature": current["temperature_2m"],
"windspeed": current["wind_speed_10m"],
"humidity": current["relative_humidity_2m"]
}
except Exception as e:
return {
"temperature": None,
"windspeed": None,
"humidity": None,
"error": str(e)
}
🔌 routers.py
¶
pAPI provides the sql_session
dependency, which you can use directly as a router dependency in your route functions. Alternatively, you can use the asynchronous context manager get_sql_session
that yields a SQLAlchemy session within an async context.
from fastapi import Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.future import select
from papi.core.db import sql_session, get_sql_session
from papi.core.router import RESTRouter
from . import models, schemas
from .crud import get_weather
router = RESTRouter()
@router.post("/stations", response_model=schemas.WeatherStationOut)
async def create_station(
station: schemas.WeatherStationCreate, db: AsyncSession = Depends(sql_session)
):
new_station = models.WeatherStation(
name=station.name, latitude=station.latitude, longitude=station.longitude
)
db.add(new_station)
await db.commit()
await db.refresh(new_station)
return new_station
@router.get("/stations", response_model=list[schemas.WeatherStationOut])
async def list_stations(db: AsyncSession = Depends(sql_session)):
result = await db.execute(select(models.WeatherStation))
return result.scalars().all()
@router.get("/stations/{station_id}/weather", response_model=schemas.WeatherReadingOut)
async def get_current_weather(station_id: int):
# Using get_sql_session context here
async with get_sql_session() as session:
result = await session.execute(
select(models.WeatherStation).where(models.WeatherStation.id == station_id)
)
station = result.scalar_one_or_none()
if not station:
raise HTTPException(status_code=404, detail="Station not found")
weather = get_weather(station.latitude, station.longitude)
reading = models.WeatherReading(
station_id=station.id,
temperature=weather["temperature"],
windspeed=weather["windspeed"],
humidity=weather["humidity"],
)
session.add(reading)
await session.commit()
await session.refresh(reading)
return reading
📆 __init__.py
¶
from . import models, routers
__all__ = ["router","models"]
⚙️ Main papi configuration (config.yaml
)¶
# Base configuration – see the Hello World example
...
# SQLAlchemy connection settings (example using SQLite)
database:
sqlalchemy_uri: "sqlite+aiosqlite:///./weather.db"
backends:
sqlalchemy:
echo: false # Optional: enables SQL query logging
# Enable the weather addon
addons:
extra_addons_path: "my_addons"
enabled:
- weather
pAPI allows fine-tuning of the database engine by providing additional configuration under the backends
section in config.yaml
.
🚜 How to Use¶
🚀 Start the API Server¶
rye run python papi/cli.py webserver
Add a station:
curl -X 'POST' \
'http://127.0.0.1:8000/stations' \
-H 'accept: application/json' \
-H 'Content-Type: application/json' \
-d '{
"name": "Santa Clara, Cuba",
"latitude": 22.4067,
"longitude": -79.9531
}'
Response:
{
"id": 1,
"name": "Santa Clara, Cuba",
"latitude": 22.4067,
"longitude": -79.9531,
"created_at": "2025-06-25T14:00:14.162273"
}
List all stations:
curl -X 'GET' \
'http://localhost:8080/stations' \
-H 'accept: application/json'
Response:
[
{
"id": 1,
"name": "Santa Clara, Cuba",
"latitude": 22.4067,
"longitude": -79.9531,
"created_at": "2025-06-25T14:00:14.162273"
}
]
Get weather data for station 1:
curl -X 'GET' \
'http://localhost:8000/stations/1/weather' \
-H 'accept: application/json'
Response:
{
"id": 1,
"station_id": 1,
"temperature": 28.3,
"windspeed": 15.4,
"humidity": 71.0,
"timestamp": "2025-06-25T14:05:48.300713"
}
✅ What's Next?¶
- Serve static files