Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions .claude/skills/meteo/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
name: meteo
description: Access weather and climate data for thousands of weather stations and any place worldwide.
---

# Meteo

## Usage

If the `meteo` command is not found, install the tool using:

```bash
uv tool install meteostat-cli
```

Alternatively, you can use `uvx`:

```bash
uvx --from meteostat-cli meteo
```

## Commands

Run `meteo --help` to see a list of all available commands and options.

## Gotchas

- `meteo nearby` requires numeric `LAT,LON` argument — city names are not accepted. Use coordinates directly.
- When looking for stations that match the elevation of a place, use `meteo nearby` without a radius and filter results by elevation.
- Pass multiple station IDs in one call instead of looping: `meteo daily D1424 EDEV0 10635 --start 2025 --end 2025`
- Fetch only the columns you need with `--parameters` to eliminate post-processing: `--parameters tmax` or `--parameters tmax,prcp`
- Aggregate all rows with `--agg` (Pandas `.agg()`): `--agg max`, `--agg mean`, `--agg sum`
- `--start` and `--end` accept YYYY-MM-DD, YYYY-MM, YYYY
- Machine-readable output for piping: `--format json` or `--format csv`

## Additional resources

- For usage examples, see [examples.md](examples.md)
72 changes: 72 additions & 0 deletions .claude/skills/meteo/examples.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Examples

## Fetch weather data

```bash
meteo daily 10637 # Station ID for Frankfurt/Main
meteo hourly 10637 --start 2024-01-01 --end 2024-01-31
meteo daily 50.1109,8.6821 --start 2024-01-01 --end 2024-12-31 # Geo coordinates (interpolated)
meteo monthly 10637 --start 2020 --end 2024 --parameters temp,tmin,tmax,prcp
meteo normals 10637
meteo hourly 10637 10635 --start 2024-06-01 --end 2024-06-30 --timezone Europe/Berlin
meteo daily 10637 10635 --start 2025-01-01 --end 2025-12-31 --agg max # Custom aggregation (Pandas `.agg()`) across multiple stations
```

## Export data

```bash
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --output data.csv
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --output data.json
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --output data.xlsx
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --output data.parquet
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --output chart.png
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --format csv --no-header
```

## Station discovery

```bash
meteo station 10637 # Metadata for a specific station
meteo station --country DE # All stations in Germany
meteo station --country DE --state HE
meteo station --name "Frankfurt" # Search by name (case-insensitive, partial match)
meteo station --bbox "8.0,50.0,10.0,51.0"
meteo station --sql "SELECT * FROM stations WHERE country = 'DE' AND elevation > 500"
meteo nearby 50.1109,8.6821 --limit 10 --radius 20000
```

## Data quality & filtering

```bash
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --with-sources # Include source provider columns
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --no-models # Exclude model data
meteo daily 10637 10635 --start 2024-01-01 --end 2024-12-31 --agg mean
meteo daily 10637 --no-cache # Disable cache for the latest data
meteo daily 10637 --start 2024-01-01 --end 2024-12-31 --all # Show all rows without truncation
```

## Finding the hottest/coldest day

```bash
# Peak tmax for a station over a full year — single command, no awk needed
meteo daily D1424 --start 2025-01-01 --end 2025-12-31 --parameters tmax --agg max -f csv
# To also get the date of the peak, filter with sort after fetching tmax only
meteo daily D1424 --start 2025-01-01 --end 2025-12-31 --parameters tmax -f csv \
| awk -F',' 'NR>1' | sort -t',' -k3 -rn | head -1
```

## Inventory

```bash
meteo inventory 10637
meteo inventory 10637 --granularity daily
meteo inventory 10637 --parameters temp,tmin,tmax
```

## Configuration

```bash
meteo config --list
meteo config cache_enable false
meteo config interpolation_radius 50000
```
1 change: 0 additions & 1 deletion meteo/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ def _register_commands() -> None:

app.command("config")(config_cmd)
app.command("station")(station_cmd)
app.command("stations")(station_cmd)
app.command("s", hidden=True)(station_cmd)
app.command("nearby")(nearby_cmd)
app.command("inventory")(inventory_cmd)
Expand Down
27 changes: 18 additions & 9 deletions meteo/commands/nearby.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@


def nearby_cmd(
lat: float = typer.Argument(..., help="Latitude."),
lon: float = typer.Argument(..., help="Longitude."),
coords: str = typer.Argument(..., help="Coordinates as lat,lon (e.g. 48.1,11.6)."),
limit: int = typer.Option(5, "--limit", "-l", help="Maximum number of stations."),
radius: int = typer.Option(5000, "--radius", "-r", help="Search radius in meters."),
radius: int | None = typer.Option(
None, "--radius", "-r", help="Search radius in meters."
),
fmt: str | None = typer.Option(
None, "--format", "-f", help="Output format (csv, json, xlsx, parquet)."
),
Expand All @@ -24,17 +25,25 @@ def nearby_cmd(
"""Find weather stations near a location."""
import meteostat as ms

if not (-90 <= lat <= 90):
typer.echo(f"Error: Latitude must be between -90 and 90, got {lat}.", err=True)
raise typer.Exit(2)
if not (-180 <= lon <= 180):
parts = coords.split(",", 1)
if len(parts) != 2:
typer.echo(
f"Error: Longitude must be between -180 and 180, got {lon}.", err=True
"Error: Coordinates must be in lat,lon format (e.g. 48.1,11.6).", err=True
)
raise typer.Exit(2)
try:
lat = float(parts[0])
lon = float(parts[1])
except ValueError:
typer.echo(f"Error: Invalid coordinates: {coords}", err=True)
raise typer.Exit(2) from None
if not (-90 <= lat <= 90):
raise typer.BadParameter(f"Latitude must be between -90 and 90, got {lat}")
if not (-180 <= lon <= 180):
raise typer.BadParameter(f"Longitude must be between -180 and 180, got {lon}")

point = ms.Point(lat, lon)
df = ms.stations.nearby(point, radius=radius, limit=limit)
df = ms.stations.nearby(point, radius=radius or 40_075_000, limit=limit)

actual_fmt = detect_format(fmt, output)
output_df(df, actual_fmt, output, no_header, show_all=show_all)
1 change: 1 addition & 0 deletions meteo/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
# CLI-specific config keys (not part of Meteostat library config)
CLI_CONFIG_KEYS = {
"interpolation_radius": 25000,
"interpolation_station_count": 10,
"humanize": True,
}

Expand Down
21 changes: 18 additions & 3 deletions meteo/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,15 @@ def get_interpolation_radius() -> int:
return CLI_CONFIG_KEYS["interpolation_radius"]


def get_interpolation_station_count() -> int:
"""Get the configured station count limit for coordinate-based queries."""
data = load_config()
key = "interpolation_station_count"
if key in data:
return int(data[key])
return CLI_CONFIG_KEYS["interpolation_station_count"]


def detect_format(fmt: str | None, output: str | None) -> str:
"""Detect the output format from explicit flag or file extension."""
if fmt is not None:
Expand Down Expand Up @@ -362,7 +371,9 @@ def fetch_timeseries(
else:
point = ms.Point(resolved[1], resolved[2])
radius = get_interpolation_radius()
nearby = ms.stations.nearby(point, radius=radius)
nearby = ms.stations.nearby(
point, radius=radius, limit=get_interpolation_station_count()
)
if nearby.empty:
typer.echo(
"No weather stations found within the specified radius.",
Expand Down Expand Up @@ -397,7 +408,9 @@ def fetch_timeseries(
else:
point = ms.Point(resolved[1], resolved[2])
radius = get_interpolation_radius()
nearby = ms.stations.nearby(point, radius=radius)
nearby = ms.stations.nearby(
point, radius=radius, limit=get_interpolation_station_count()
)
if nearby.empty:
typer.echo(
"No weather stations found within the specified radius.",
Expand Down Expand Up @@ -434,7 +447,9 @@ def fetch_timeseries(
else:
point = ms.Point(resolved[1], resolved[2])
radius = get_interpolation_radius()
nearby = ms.stations.nearby(point, radius=radius)
nearby = ms.stations.nearby(
point, radius=radius, limit=get_interpolation_station_count()
)
if nearby.empty:
typer.echo(
"No weather stations found within the specified radius.",
Expand Down
6 changes: 0 additions & 6 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,12 +67,6 @@ def test_command_help_config(invoke):
assert result.exit_code == 0


def test_command_help_stations(invoke):
"""Test that 'stations --help' works."""
result = invoke("stations", "--help")
assert result.exit_code == 0


def test_command_help_nearby(invoke):
"""Test that 'nearby --help' works."""
result = invoke("nearby", "--help")
Expand Down
13 changes: 6 additions & 7 deletions tests/test_commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def test_nearby_basic(self, invoke):
with patch("meteostat.stations") as mock_stations:
mock_stations.nearby.return_value = df
with patch("meteostat.Point"):
result = invoke("nearby", "50.1109", "8.6821")
result = invoke("nearby", "50.1109,8.6821")
assert result.exit_code == 0
assert "Frankfurt" in result.output

Expand Down Expand Up @@ -259,7 +259,7 @@ def test_nearby_all_flag(self, invoke):
with patch("meteostat.stations") as mock_stations:
mock_stations.nearby.return_value = df
with patch("meteostat.Point"):
result = invoke("nearby", "50.1109", "8.6821", "--all")
result = invoke("nearby", "50.1109,8.6821", "--all")
assert result.exit_code == 0

def test_hourly_all_flag(self, invoke):
Expand Down Expand Up @@ -311,7 +311,6 @@ def test_all_flag_in_help(self, invoke):
"""--all flag appears in help for all data commands."""
for cmd in [
"station",
"stations",
"nearby",
"inventory",
"hourly",
Expand Down Expand Up @@ -544,7 +543,7 @@ def test_nearby_no_results(self, invoke):
"""Nearby command exits 1 when no stations found."""
with patch("meteostat.stations") as ms, patch("meteostat.Point"):
ms.nearby.return_value = pd.DataFrame()
result = invoke("nearby", "50.1109", "8.6821")
result = invoke("nearby", "50.1109,8.6821")
assert result.exit_code == 1
assert "No data" in result.output

Expand All @@ -556,19 +555,19 @@ def test_nearby_csv_format(self, invoke):
)
with patch("meteostat.stations") as ms, patch("meteostat.Point"):
ms.nearby.return_value = df
result = invoke("nearby", "50.1109", "8.6821", "--format", "csv")
result = invoke("nearby", "50.1109,8.6821", "--format", "csv")
assert result.exit_code == 0
assert "name" in result.output

def test_nearby_invalid_latitude(self, invoke):
"""Nearby command exits 2 when latitude is out of range."""
result = invoke("nearby", "200", "8.6821")
result = invoke("nearby", "200,8.6821")
assert result.exit_code == 2
assert "Latitude" in result.output

def test_nearby_invalid_longitude(self, invoke):
"""Nearby command exits 2 when longitude is out of range."""
result = invoke("nearby", "50.1109", "200")
result = invoke("nearby", "50.1109,200")
assert result.exit_code == 2
assert "Longitude" in result.output

Expand Down
Loading