Metadata-Version: 2.4
Name: pyopenmeteo
Version: 0.1.0
Summary: A Python wrapper for the Open-Meteo weather API family
Author: jania
License-Expression: MIT
Project-URL: Homepage, https://github.com/jania/pyopenmeteo
Project-URL: Issues, https://github.com/jania/pyopenmeteo/issues
Keywords: weather,forecast,open-meteo,climate,meteorology
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Atmospheric Science
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE.md
Requires-Dist: requests>=2.28
Provides-Extra: pandas
Requires-Dist: pandas>=1.5; extra == "pandas"
Requires-Dist: openpyxl>=3.0; extra == "pandas"
Provides-Extra: numpy
Requires-Dist: numpy>=1.24; extra == "numpy"
Provides-Extra: xarray
Requires-Dist: xarray>=2023.1; extra == "xarray"
Requires-Dist: numpy>=1.24; extra == "xarray"
Provides-Extra: all
Requires-Dist: pandas>=1.5; extra == "all"
Requires-Dist: openpyxl>=3.0; extra == "all"
Requires-Dist: numpy>=1.24; extra == "all"
Requires-Dist: xarray>=2023.1; extra == "all"
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Requires-Dist: pandas>=1.5; extra == "dev"
Requires-Dist: numpy>=1.24; extra == "dev"
Requires-Dist: xarray>=2023.1; extra == "dev"
Requires-Dist: openpyxl>=3.0; extra == "dev"
Dynamic: license-file

﻿# pyopenmeteo

A Python wrapper for the [Open-Meteo](https://open-meteo.com/) family of weather APIs.

Open-Meteo is one of the best free weather data sources available. It has global coverage, hourly resolution, 80+ years of historical data, ensemble forecasts, climate projections, air quality, marine data, and more -- all with no API key.

pyopenmeteo wraps the API so you don't have to hand-build query strings, deal with raw JSON, or look up parameter names every time. Pass a city name or coordinates, get back a pandas DataFrame.

---

## Contents

- [What's included](#whats-included)
- [Installation](#installation)
- [Quick start](#quick-start)
- [APIs included](#apis-included)
  - [ForecastAPI](#forecastapi)
  - [HistoricalAPI](#historicalapi)
  - [MarineAPI](#marineapi)
  - [AirQualityAPI](#airqualityapi)
  - [ClimateAPI](#climateapi)
  - [EnsembleAPI](#ensembleapi)
  - [FloodAPI](#floodapi)
  - [SeasonalAPI](#seasonalapi)
  - [GeocodingAPI & ElevationAPI](#geocodingapi--elevationapi)
- [Working with responses](#working-with-responses)
- [Params](#params)
- [Units and options](#units-and-options)
- [Error handling](#error-handling)
- [Commercial API key](#commercial-api-key)
- [License](#license)

### Full documentation

| Doc | Description |
|-----|-------------|
| [Quick start](docs/quickstart.md) | Minimal working examples for each API |
| [Forecast](docs/forecast.md) | ForecastAPI reference |
| [Historical](docs/historical.md) | HistoricalAPI reference |
| [Marine](docs/marine.md) | MarineAPI reference |
| [Air quality](docs/air_quality.md) | AirQualityAPI reference |
| [Climate](docs/climate.md) | ClimateAPI reference |
| [Ensemble](docs/ensemble.md) | EnsembleAPI reference |
| [Flood](docs/flood.md) | FloodAPI reference |
| [Seasonal](docs/seasonal.md) | SeasonalAPI reference |
| [Satellite](docs/satellite.md) | SatelliteRadiationAPI reference |
| [Geocoding & elevation](docs/geo.md) | GeocodingAPI and ElevationAPI reference |
| [Response](docs/response.md) | WeatherResponse methods and formatters |
| [Params](docs/params.md) | Variable enums and units |
| [Exceptions](docs/exceptions.md) | Exception hierarchy |

---

## What's included

| API | What it gives you | Horizon |
|-----|-------------------|---------|
| `ForecastAPI` | Hourly, 15-min, daily, current-conditions forecasts | 16 days |
| `HistoricalAPI` | Reanalysis data back to 1940 | any date range |
| `HistoricalForecastAPI` | Archived model runs | any date range |
| `MarineAPI` | Wave height, swell, currents | 16 days |
| `ClimateAPI` | CMIP6 climate projections | 1950–2100 |
| `AirQualityAPI` | Pollutants, pollen, AQI | 7 days |
| `EnsembleAPI` | Probabilistic ensemble runs | 35 days |
| `FloodAPI` | River discharge forecasts | 210 days |
| `SatelliteRadiationAPI` | Solar irradiance from satellites | historical + near-realtime |
| `SeasonalAPI` | Long-range seasonal forecasts | 274 days |
| `GeocodingAPI` | Fuzzy place-name → coordinates | - |
| `ElevationAPI` | Coordinates → elevation (Copernicus DEM) | - |

---

## Installation

```bash
pip install pyopenmeteo
```

Optional extras for data conversion:

```bash
pip install pyopenmeteo[pandas]   # to_pandas(), to_csv(), to_excel()
pip install pyopenmeteo[numpy]    # to_numpy()
pip install pyopenmeteo[xarray]   # formatters.to_dataset()
pip install pyopenmeteo[all]      # everything above
```

---

## Quick start

```python
from pyopenmeteo.api.forecast import ForecastAPI
from pyopenmeteo.params.forecast_vars import HourlyForecastVar, DailyForecastVar

api = ForecastAPI()

# Pass a place name - geocoding is handled automatically
resp = api.get(
    "Toronto",
    hourly=[HourlyForecastVar.TEMPERATURE_2M, HourlyForecastVar.PRECIPITATION],
    daily=[DailyForecastVar.TEMPERATURE_2M_MAX, DailyForecastVar.PRECIPITATION_SUM],
    timezone="America/Toronto",
)

print(resp)
# WeatherResponse(loc=43.7001,-79.4163; hourly×168, daily×7)

df = resp.to_pandas("hourly")
print(df.head())
```

Or pass coordinates directly:

```python
resp = api.get(
    (43.7001, -79.4163),
    hourly=[HourlyForecastVar.WIND_SPEED_10M],
    forecast_days=3,
)
```

---

## APIs included

### ForecastAPI

Up to 16 days of forecast data at any location.

```python
from pyopenmeteo.api.forecast import ForecastAPI
from pyopenmeteo.params.forecast_vars import HourlyForecastVar, DailyForecastVar, CurrentForecastVar
from pyopenmeteo.params.units import TemperatureUnit, WindSpeedUnit

api = ForecastAPI()

resp = api.get(
    "London",
    hourly=[
        HourlyForecastVar.TEMPERATURE_2M,
        HourlyForecastVar.RELATIVE_HUMIDITY_2M,
        HourlyForecastVar.WIND_SPEED_10M,
        HourlyForecastVar.PRECIPITATION_PROBABILITY,
    ],
    daily=[
        DailyForecastVar.TEMPERATURE_2M_MAX,
        DailyForecastVar.TEMPERATURE_2M_MIN,
        DailyForecastVar.PRECIPITATION_SUM,
        DailyForecastVar.SUNRISE,
        DailyForecastVar.SUNSET,
    ],
    current=[CurrentForecastVar.TEMPERATURE_2M],
    temperature_unit=TemperatureUnit.FAHRENHEIT,
    wind_speed_unit=WindSpeedUnit.MPH,
    timezone="Europe/London",
    forecast_days=7,
)
```

**Pressure-level variables** (upper atmosphere data):

```python
from pyopenmeteo.params.pressure_levels import PressureVar, pressure_level, pressure_levels_range

resp = api.get(
    (43.65, -79.38),
    hourly=[
        pressure_level(PressureVar.TEMPERATURE, 850),      # "temperature_850hPa"
        pressure_level(PressureVar.WIND_SPEED, 500),        # "wind_speed_500hPa"
        *pressure_levels_range(PressureVar.GEOPOTENTIAL_HEIGHT, 500, 1000),
    ],
)
```

Valid pressure levels: 30, 50, 70, 100, 150, 200, 250, 300, 400, 500, 600, 700, 800, 850, 900, 925, 950, 975, 1000 hPa.

**Solar panel irradiance** (GTI variables require a panel orientation):

```python
from pyopenmeteo.params.solar import PanelOrientation
from pyopenmeteo.params.forecast_vars import HourlyForecastVar

resp = api.get(
    (43.65, -79.38),
    hourly=[HourlyForecastVar.GLOBAL_TILTED_IRRADIANCE],
    panel=PanelOrientation(tilt=35, azimuth=0),  # south-facing, 35° tilt
)

# Single-axis tracker: pass float("nan") for the tracked axis
panel = PanelOrientation(tilt=35, azimuth=float("nan"))
```

---

### HistoricalAPI

Reanalysis data from 1940 onward. Requires explicit date range and at least one variable group.

```python
from pyopenmeteo.api.historical import HistoricalAPI
from pyopenmeteo.params.archive_vars import HourlyArchiveVars, DailyArchiveVars

api = HistoricalAPI()
resp = api.get(
    "Berlin",
    start_date="2020-01-01",
    end_date="2020-12-31",
    hourly=[HourlyArchiveVars.TEMPERATURE_2M, HourlyArchiveVars.PRECIPITATION],
    daily=[DailyArchiveVars.TEMPERATURE_2M_MAX],
    timezone="Europe/Berlin",
)
```

---

### MarineAPI

Wave and swell forecasts up to 16 days.

```python
from pyopenmeteo.api.marine import MarineAPI
from pyopenmeteo.params.marine_vars import HourlyMarineVars, DailyMarineVars

api = MarineAPI()
resp = api.get(
    (51.5, -1.8),   # English Channel
    hourly=[HourlyMarineVars.WAVE_HEIGHT, HourlyMarineVars.WAVE_DIRECTION],
    daily=[DailyMarineVars.WAVE_HEIGHT_MAX],
)
```

---

### AirQualityAPI

Pollutant concentrations, pollen levels, and AQI up to 7 days ahead.

```python
from pyopenmeteo.api.air_quality import AirQualityAPI
from pyopenmeteo.params.airquality_vars import HourlyAirQualityVars

api = AirQualityAPI()
resp = api.get(
    "Beijing",
    hourly=[
        HourlyAirQualityVars.PM2_5,
        HourlyAirQualityVars.PM10,
        HourlyAirQualityVars.EUROPEAN_AQI,
    ],
    timezone="Asia/Shanghai",
)
```

---

### ClimateAPI

CMIP6 climate projections at 10 km resolution from 1950 to 2100. Requires a date range and daily variables.

```python
from pyopenmeteo.api.climate import ClimateAPI
from pyopenmeteo.params.climate_vars import DailyClimateVars, ClimateModels

api = ClimateAPI()
resp = api.get(
    "New York",
    start_date="2050-01-01",
    end_date="2050-12-31",
    daily=[DailyClimateVars.TEMPERATURE_2M_MAX, DailyClimateVars.PRECIPITATION_SUM],
    models=[ClimateModels.MRI_AGCM3_2_S, ClimateModels.EC_EARTH3P_HR],
)
```

---

### EnsembleAPI

Probabilistic forecasts from multiple ensemble members, up to 35 days.

```python
from pyopenmeteo.api.ensemble import EnsembleAPI
from pyopenmeteo.params.ensemble_vars import HourlyEnsembleVars, EnsembleModels

api = EnsembleAPI()
resp = api.get(
    "Tokyo",
    hourly=[HourlyEnsembleVars.TEMPERATURE_2M, HourlyEnsembleVars.PRECIPITATION],
    models=[EnsembleModels.ICON_EU_EPS, EnsembleModels.NCEP_GEFS_SEAMLESS],
)
```

---

### FloodAPI

River discharge forecasts up to 210 days.

```python
from pyopenmeteo.api.flood import FloodAPI
from pyopenmeteo.params.flood_vars import DailyFloodVars

api = FloodAPI()
resp = api.get(
    (48.2, 16.4),   # near Vienna on the Danube
    daily=[DailyFloodVars.RIVER_DISCHARGE],
    ensemble=True,
)
```

---

### SeasonalAPI

Long-range seasonal forecasts at weekly or monthly resolution up to 274 days.

```python
from pyopenmeteo.api.seasonal import SeasonalAPI
from pyopenmeteo.params.seasonal_vars import WeeklySeasonalVars

api = SeasonalAPI()
resp = api.get(
    "Lagos",
    weekly=[WeeklySeasonalVars.TEMPERATURE_2M_MEAN],
    timezone="Africa/Lagos",
)
```

---

### GeocodingAPI & ElevationAPI

```python
from pyopenmeteo.api.geo import GeocodingAPI, ElevationAPI

geo = GeocodingAPI()

# Fuzzy search - returns the top result
loc = geo.search("paris")
print(loc.name, loc.latitude, loc.longitude, loc.country)
# Paris 48.8534 2.3488 France

# All results
results = geo.search_all("paris", count=5)

# Look up by GeoNames ID
loc = geo.get_by_id(6455259)

# Elevation lookup
elev = ElevationAPI()
altitude = elev.get(48.85, 2.35)          # scalar → float
altitudes = elev.get([48.85, 51.5], [2.35, -0.12])  # lists → list[float]
```

---

## Working with responses

Every API returns a `WeatherResponse` object.

```python
resp = api.get(...)

# Inspect what's available
resp.has_hourly()       # True / False
resp.has_daily()
resp.has_minutely_15()
resp.has_current()

resp.get_hourly()       # ["temperature_2m", "precipitation", ...]
resp.get_daily()

# Metadata
resp.latitude
resp.longitude
resp.elevation
resp.timezone

# Raw JSON
resp.to_dict()

# pandas DataFrame (indexed by DatetimeIndex)
df = resp.to_pandas("hourly")
df = resp.to_pandas("daily")
df = resp.to_pandas("current")

# NumPy arrays
arrays = resp.to_numpy("hourly")
# {"time": array(['2024-01-01T00:00:00', ...], dtype='datetime64[s]'),
#  "temperature_2m": array([...], dtype=float64)}

# Write to disk
resp.to_csv("weather.csv", section="hourly")
resp.to_excel("weather.xlsx", section="daily")
```

### Standalone formatters

The `formatters` module provides functions that work with any response:

```python
from pyopenmeteo.formatters import to_dataframe, to_arrays, to_csv, to_dataset

df = to_dataframe(resp, section="hourly")
arrays = to_arrays(resp, section="daily")
to_csv(resp, "out.csv", section="hourly")
ds = to_dataset(resp, section="hourly")   # xarray Dataset
```

---

## Params

Every variable you can request from an API is defined as a `StrEnum` in `pyopenmeteo.params`. Using these instead of raw strings gives you IDE autocomplete, catches typos at import time, and makes it easy to discover what's available without reading the Open-Meteo docs.

Each enum member's value is exactly the string the API expects, so you can always drop down to a raw string if you need a variable that isn't in the enum yet.

| Module | Classes |
|--------|---------|
| `params.forecast_vars` | `HourlyForecastVar`, `Minutely15ForecastVar`, `DailyForecastVar`, `CurrentForecastVar`, `ForecastModels` |
| `params.archive_vars` | `HourlyArchiveVars`, `DailyArchiveVars`, `ArchiveModels` |
| `params.marine_vars` | `HourlyMarineVars`, `Minutely15MarineVars`, `DailyMarineVars`, `CurrentMarineVars`, `MarineModels` |
| `params.airquality_vars` | `HourlyAirQualityVars`, `CurrentAirQualityVars` |
| `params.climate_vars` | `DailyClimateVars`, `ClimateModels` |
| `params.ensemble_vars` | `HourlyEnsembleVars`, `DailyEnsembleVars`, `EnsembleModels` |
| `params.flood_vars` | `DailyFloodVars`, `FloodModels` |
| `params.satellite_vars` | `HourlySatelliteVars`, `DailySatelliteVars`, `SatelliteModels` |
| `params.seasonal_vars` | `HourlyVars`, `DailySeasonalVars`, `WeeklySeasonalVars`, `MonthlySeasonalVars`, `SeasonalModels` |
| `params.pressure_levels` | `PressureVar`, `pressure_level()`, `pressure_levels_range()` |
| `params.solar` | `PanelOrientation` |
| `params.units` | `TemperatureUnit`, `WindSpeedUnit`, `PrecipitationUnit`, `TimeFormat`, `CellSelection`, `LengthUnit` |

```python
from pyopenmeteo.params.forecast_vars import HourlyForecastVar

# Autocomplete shows every available variable
HourlyForecastVar.TEMPERATURE_2M        # "temperature_2m"
HourlyForecastVar.WIND_SPEED_10M        # "wind_speed_10m"
HourlyForecastVar.PRECIPITATION         # "precipitation"

# Raw strings work anywhere an enum is accepted
resp = api.get("Oslo", hourly=["temperature_2m", HourlyForecastVar.PRECIPITATION])
```

---

## Units and options

```python
from pyopenmeteo.params.units import (
    TemperatureUnit,    # CELSIUS, FAHRENHEIT
    WindSpeedUnit,      # KMH, MS, MPH, KN
    PrecipitationUnit,  # MM, INCH
    TimeFormat,         # ISO8601, UNIXTIME
    CellSelection,      # LAND, SEA, NEAREST
    LengthUnit,         # METRIC, IMPERIAL
)
```

---

## Error handling

```python
from pyopenmeteo.core.exceptions import (
    MeteoPyError,           # catch-all base class
    APIError,               # 4xx from the API (has .status_code, .reason)
    RateLimitError,         # 429 - subclass of APIError
    ServerError,            # 5xx - subclass of APIError
    GeocodingError,         # base for location errors
    LocationNotFoundError,  # place name returned zero results (has .query)
    ValidationError,        # bad parameters before any HTTP call
    ConnectionError,        # network unreachable
)

from pyopenmeteo.api.forecast import ForecastAPI

api = ForecastAPI()
try:
    resp = api.get("Atlantis", hourly=["temperature_2m"])
except LocationNotFoundError as e:
    print(f"Could not find: {e.query}")
except RateLimitError:
    print("Hit the rate limit - add an API key or slow down")
except APIError as e:
    print(f"API error {e.status_code}: {e.reason}")
except MeteoPyError as e:
    print(f"Something went wrong: {e}")
```

---

## Commercial API key

Open-Meteo offers a [commercial API](https://open-meteo.com/en/pricing) for higher rate limits. Pass the key to any API client:

```python
api = ForecastAPI(apikey="your-key-here")
```

---

## License

This project is licensed under the [MIT License](LICENSE).
