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
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: "Set up Python"
uses: actions/setup-python@v6.1.0
with:
python-version: "3.12.4"
python-version: "3.x"

- name: "Install requirements"
run: |
Expand Down
29 changes: 29 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
name: "Tests"

on:
pull_request:

permissions:
contents: read

jobs:
test:
name: "Run Tests"
runs-on: "ubuntu-latest"
steps:
- name: "Checkout the repository"
uses: "actions/checkout@v6.0.1"

- name: "Set up Python"
uses: actions/setup-python@v6.1.0
with:
python-version: "3.x"

- name: "Install requirements"
run: |
python -m pip install --upgrade pip
pip install -r requirements-ci.txt

- name: "Run tests"
run: |
python -m pytest tests/ -v
70 changes: 70 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@ This library for the [Pirate Weather API](https://pirateweather.net) which is an
API, and provides access to detailed
weather information from around the globe.

**Now supporting Pirate Weather API v2.8+** with new data fields including smoke levels, solar radiation, fire indices, and detailed precipitation breakdowns.

* [Installation](#installation)
* [Get started](#get-started)
* [New Features](#new-features)
* [Contact us](#contact-us)
* [License](#license)

Expand Down Expand Up @@ -138,6 +141,73 @@ api_key = "0123456789"
asyncio.run(main(api_key))
```

### New Features

#### API v2.8+ Support

This library now supports all Pirate Weather API v2.8+ fields:

**New Weather Data Fields:**
- `smoke` - Air quality smoke levels (µg/m³)
- `solar` - Solar radiation (W/m²)
- `feelsLike` - Apparent temperature based on wind and humidity
- `cape` - Convective Available Potential Energy
- `fireIndex` - Fire weather index
- `liquidAccumulation`, `snowAccumulation`, `iceAccumulation` - Precipitation by type
- `rainIntensity`, `snowIntensity`, `iceIntensity` - Intensity by precipitation type
- `currentDayIce`, `currentDayLiquid`, `currentDaySnow` - Accumulations for the current day
- `dawnTime`, `duskTime` - Civil twilight times

**New Metadata Fields:**
- `sourceTimes` - Model update timestamps
- `sourceIDX` - Grid coordinates for each model
- `version` - API version
- `processTime` - Request processing time
- `ingestVersion` - Data ingest version
- `nearestCity`, `nearestCountry`, `nearestSubNational` - Location information

**Day/Night Forecast Block:**

The library now supports the optional `day_night` forecast block which provides 12-hour forecast periods:

```python
forecast = pirate_weather.get_forecast(latitude, longitude)

# Access day/night forecast data
for period in forecast.day_night.data:
print(f"Time: {period.time}, Temp: {period.temperature}, Smoke: {period.smoke}")
```

**Example accessing new fields:**

```python
forecast = pirate_weather.get_forecast(latitude, longitude)

# Current conditions with new fields
print(f"Current smoke level: {forecast.currently.smoke} µg/m³")
print(f"Solar radiation: {forecast.currently.solar} W/m²")
print(f"Feels like: {forecast.currently.feels_like}°")
print(f"Fire index: {forecast.currently.fire_index}")

# Hourly forecasts with precipitation breakdowns
for hour in forecast.hourly.data:
if hour.rain_intensity > 0:
print(f"Rain intensity: {hour.rain_intensity} mm/h")
if hour.snow_intensity > 0:
print(f"Snow intensity: {hour.snow_intensity} cm/h")

# Daily forecasts with new max fields
for day in forecast.daily.data:
print(f"Max smoke: {day.smoke_max} at {day.smoke_max_time}")
print(f"Max solar: {day.solar_max} at {day.solar_max_time}")
print(f"Dawn: {day.dawn_time}, Dusk: {day.dusk_time}")

# Metadata
print(f"API Version: {forecast.flags.version}")
print(f"Nearest City: {forecast.flags.nearest_city}")
print(f"Process Time: {forecast.flags.process_time}ms")
```

### License.

Library is released under the [MIT License](./LICENSE).
36 changes: 35 additions & 1 deletion pirate_weather/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ def get_recent_time_machine_forecast(
current_time = int(datetime.now().timestamp())
if timezone:
tz = pytz.timezone(timezone)
current_time = datetime.now(tz)
current_time = int(datetime.now(tz).timestamp())

diff = required_time - current_time

Expand Down Expand Up @@ -228,3 +228,37 @@ async def get_time_machine_forecast(
session=client_session,
)
return Forecast(**data)

async def get_recent_time_machine_forecast(
self,
latitude: float,
longitude: float,
time: datetime,
client_session: aiohttp.ClientSession,
extend: bool = None,
lang=Languages.ENGLISH,
values_units=Units.AUTO,
exclude: [Weather] = None,
timezone: str = None,
) -> Forecast:
required_time = int(time.timestamp())
current_time = int(datetime.now().timestamp())
if timezone:
tz = pytz.timezone(timezone)
current_time = int(datetime.now(tz).timestamp())

diff = required_time - current_time

exclude = self.convert_exclude_param_to_string(exclude)

url = self.get_url(latitude, longitude, diff)
data = await self.request_manager.make_request(
url=url,
extend=Weather.HOURLY if extend else None,
lang=lang,
units=values_units,
exclude=exclude,
timezone=timezone,
session=client_session,
)
return Forecast(**data)
7 changes: 4 additions & 3 deletions pirate_weather/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,13 @@ class AutoInit:
def __init__(self, **params):
try:
timezone = pytz.timezone(params.pop("timezone", None))
except (pytz.UnknownTimeZoneError, AttributeError):
except (pytz.UnknownTimeZoneError, AttributeError, TypeError):
timezone = pytz.UTC

for field in self.__annotations__:
for field in getattr(self.__class__, "__annotations__", {}):
api_field = undo_snake_case_key(field)
if self.__annotations__[field] == datetime:
annotations = getattr(self.__class__, "__annotations__", {})
if annotations.get(field) == datetime:
params[api_field] = get_datetime_from_unix(
params.get(api_field), timezone
)
Expand Down
119 changes: 119 additions & 0 deletions pirate_weather/forecast.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,22 @@ class CurrentlyForecast(base.AutoInit):
uv_index: int
visibility: float
ozone: float
# New fields from API v2+
rain_intensity: float = None
snow_intensity: float = None
ice_intensity: float = None
smoke: float = None
solar: float = None
feels_like: float = None
cape: float = None
fire_index: float = None
liquid_accumulation: float = None
snow_accumulation: float = None
ice_accumulation: float = None
station_pressure: float = None
current_day_ice: float = None
current_day_liquid: float = None
current_day_snow: float = None


class MinutelyForecastItem(base.AutoInit):
Expand All @@ -32,6 +48,10 @@ class MinutelyForecastItem(base.AutoInit):
precip_intensity_error: float
precip_probability: float
precip_type: str
# New fields from API v2+
rain_intensity: float = None
snow_intensity: float = None
sleet_intensity: float = None


class MinutelyForecast(base.BaseWeather):
Expand All @@ -44,6 +64,7 @@ class HourlyForecastItem(base.AutoInit):
summary: str = None
icon: str
precip_intensity: float
precip_intensity_error: float = None
precip_probability: float
precip_type: str
precipAccumulation: float
Expand All @@ -59,6 +80,21 @@ class HourlyForecastItem(base.AutoInit):
uv_index: int
visibility: float
ozone: float
# New fields from API v2+
nearest_storm_distance: int = None
nearest_storm_bearing: int = None
smoke: float = None
solar: float = None
feels_like: float = None
cape: float = None
fire_index: float = None
liquid_accumulation: float = None
snow_accumulation: float = None
ice_accumulation: float = None
rain_intensity: float = None
snow_intensity: float = None
ice_intensity: float = None
station_pressure: float = None


class HourlyForecast(base.BaseWeather):
Expand Down Expand Up @@ -107,13 +143,84 @@ class DailyForecastItem(base.AutoInit):
apparent_temperature_min_time: int
apparent_temperature_max: float
apparent_temperature_max_time: int
# New fields from API v2+
rain_intensity: float = None
rain_intensity_max: float = None
rain_intensity_max_time: int = None
snow_intensity: float = None
snow_intensity_max: float = None
snow_intensity_max_time: int = None
ice_intensity: float = None
ice_intensity_max: float = None
ice_intensity_max_time: int = None
smoke_max: float = None
smoke_max_time: int = None
solar_max: float = None
solar_max_time: int = None
cape_max: float = None
cape_max_time: int = None
fire_index_max: float = None
fire_index_max_time: int = None
liquid_accumulation: float = None
snow_accumulation: float = None
ice_accumulation: float = None
current_day_ice: float = None
current_day_liquid: float = None
current_day_snow: float = None
dawn_time: int = None
dusk_time: int = None


class DailyForecast(base.BaseWeather):
data: list[DailyForecastItem]
data_class = DailyForecastItem


# DayNight block is similar to hourly but has some additional fields
class DayNightForecastItem(base.AutoInit):
time: int
summary: str = None
icon: str
precip_intensity: float
precip_intensity_max: float = None
precip_probability: float
precip_type: str
precipAccumulation: float
temperature: float
apparent_temperature: float
dew_point: float
humidity: float
pressure: float
wind_speed: float
wind_gust: float
wind_bearing: int
cloud_cover: float
uv_index: int
visibility: float
ozone: float
# Fields that may be in day_night
smoke: float = None
solar: float = None
feels_like: float = None
cape: float = None
fire_index: float = None
liquid_accumulation: float = None
snow_accumulation: float = None
ice_accumulation: float = None
rain_intensity: float = None
snow_intensity: float = None
ice_intensity: float = None
rain_intensity_max: float = None
snow_intensity_max: float = None
ice_intensity_max: float = None
station_pressure: float = None


class DayNightForecast(base.BaseWeather):
data: list[DayNightForecastItem]
data_class = DayNightForecastItem


class Alert(base.AutoInit):
title: str
regions: list
Expand All @@ -130,6 +237,15 @@ class Flags(base.AutoInit):
nearest__station: float
pirate_weather__unavailable: bool
units: str
# New fields from API v2+
source_times: dict = None
source_i_d_x: dict = None
version: str = None
process_time: float = None
ingest_version: str = None
nearest_city: str = None
nearest_country: str = None
nearest_sub_national: str = None


class Forecast:
Expand All @@ -140,6 +256,7 @@ class Forecast:
minutely: MinutelyForecast
hourly: HourlyForecast
daily: DailyForecast
day_night: DayNightForecast
alerts: list[Alert]
flags: Flags
offset: int
Expand All @@ -153,6 +270,7 @@ def __init__(
minutely: dict = None,
hourly: dict = None,
daily: dict = None,
day_night: dict = None,
alerts: [dict] = None,
flags: dict = None,
offset: int = None,
Expand All @@ -166,6 +284,7 @@ def __init__(
self.minutely = MinutelyForecast(timezone=timezone, **(minutely or {}))
self.hourly = HourlyForecast(timezone=timezone, **(hourly or {}))
self.daily = DailyForecast(timezone=timezone, **(daily or {}))
self.day_night = DayNightForecast(timezone=timezone, **(day_night or {}))

self.alerts = [Alert(timezone=timezone, **alert) for alert in (alerts or [])]
self.flags = Flags(timezone=timezone, **(flags or {}))
Expand Down
Loading