Skip to content

Commit a1c025d

Browse files
Update library to support Pirate Weather API v2.8+ fields and features (#96)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cloneofghosts <10248058+cloneofghosts@users.noreply.github.com> Co-authored-by: Kev <cloneofghosts@users.noreply.github.com>
1 parent 7ff9074 commit a1c025d

File tree

11 files changed

+515
-134
lines changed

11 files changed

+515
-134
lines changed

.github/workflows/lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ jobs:
1818
- name: "Set up Python"
1919
uses: actions/setup-python@v6.1.0
2020
with:
21-
python-version: "3.12.4"
21+
python-version: "3.x"
2222

2323
- name: "Install requirements"
2424
run: |

.github/workflows/test.yml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
name: "Tests"
2+
3+
on:
4+
pull_request:
5+
6+
permissions:
7+
contents: read
8+
9+
jobs:
10+
test:
11+
name: "Run Tests"
12+
runs-on: "ubuntu-latest"
13+
steps:
14+
- name: "Checkout the repository"
15+
uses: "actions/checkout@v6.0.1"
16+
17+
- name: "Set up Python"
18+
uses: actions/setup-python@v6.1.0
19+
with:
20+
python-version: "3.x"
21+
22+
- name: "Install requirements"
23+
run: |
24+
python -m pip install --upgrade pip
25+
pip install -r requirements-ci.txt
26+
27+
- name: "Run tests"
28+
run: |
29+
python -m pytest tests/ -v

README.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ This library for the [Pirate Weather API](https://pirateweather.net) which is an
55
API, and provides access to detailed
66
weather information from around the globe.
77

8+
**Now supporting Pirate Weather API v2.8+** with new data fields including smoke levels, solar radiation, fire indices, and detailed precipitation breakdowns.
9+
810
* [Installation](#installation)
911
* [Get started](#get-started)
12+
* [New Features](#new-features)
1013
* [Contact us](#contact-us)
1114
* [License](#license)
1215

@@ -138,6 +141,73 @@ api_key = "0123456789"
138141
asyncio.run(main(api_key))
139142
```
140143

144+
### New Features
145+
146+
#### API v2.8+ Support
147+
148+
This library now supports all Pirate Weather API v2.8+ fields:
149+
150+
**New Weather Data Fields:**
151+
- `smoke` - Air quality smoke levels (µg/m³)
152+
- `solar` - Solar radiation (W/m²)
153+
- `feelsLike` - Apparent temperature based on wind and humidity
154+
- `cape` - Convective Available Potential Energy
155+
- `fireIndex` - Fire weather index
156+
- `liquidAccumulation`, `snowAccumulation`, `iceAccumulation` - Precipitation by type
157+
- `rainIntensity`, `snowIntensity`, `iceIntensity` - Intensity by precipitation type
158+
- `currentDayIce`, `currentDayLiquid`, `currentDaySnow` - Accumulations for the current day
159+
- `dawnTime`, `duskTime` - Civil twilight times
160+
161+
**New Metadata Fields:**
162+
- `sourceTimes` - Model update timestamps
163+
- `sourceIDX` - Grid coordinates for each model
164+
- `version` - API version
165+
- `processTime` - Request processing time
166+
- `ingestVersion` - Data ingest version
167+
- `nearestCity`, `nearestCountry`, `nearestSubNational` - Location information
168+
169+
**Day/Night Forecast Block:**
170+
171+
The library now supports the optional `day_night` forecast block which provides 12-hour forecast periods:
172+
173+
```python
174+
forecast = pirate_weather.get_forecast(latitude, longitude)
175+
176+
# Access day/night forecast data
177+
for period in forecast.day_night.data:
178+
print(f"Time: {period.time}, Temp: {period.temperature}, Smoke: {period.smoke}")
179+
```
180+
181+
**Example accessing new fields:**
182+
183+
```python
184+
forecast = pirate_weather.get_forecast(latitude, longitude)
185+
186+
# Current conditions with new fields
187+
print(f"Current smoke level: {forecast.currently.smoke} µg/m³")
188+
print(f"Solar radiation: {forecast.currently.solar} W/m²")
189+
print(f"Feels like: {forecast.currently.feels_like}°")
190+
print(f"Fire index: {forecast.currently.fire_index}")
191+
192+
# Hourly forecasts with precipitation breakdowns
193+
for hour in forecast.hourly.data:
194+
if hour.rain_intensity > 0:
195+
print(f"Rain intensity: {hour.rain_intensity} mm/h")
196+
if hour.snow_intensity > 0:
197+
print(f"Snow intensity: {hour.snow_intensity} cm/h")
198+
199+
# Daily forecasts with new max fields
200+
for day in forecast.daily.data:
201+
print(f"Max smoke: {day.smoke_max} at {day.smoke_max_time}")
202+
print(f"Max solar: {day.solar_max} at {day.solar_max_time}")
203+
print(f"Dawn: {day.dawn_time}, Dusk: {day.dusk_time}")
204+
205+
# Metadata
206+
print(f"API Version: {forecast.flags.version}")
207+
print(f"Nearest City: {forecast.flags.nearest_city}")
208+
print(f"Process Time: {forecast.flags.process_time}ms")
209+
```
210+
141211
### License.
142212

143213
Library is released under the [MIT License](./LICENSE).

pirate_weather/api.py

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ def get_recent_time_machine_forecast(
157157
current_time = int(datetime.now().timestamp())
158158
if timezone:
159159
tz = pytz.timezone(timezone)
160-
current_time = datetime.now(tz)
160+
current_time = int(datetime.now(tz).timestamp())
161161

162162
diff = required_time - current_time
163163

@@ -228,3 +228,37 @@ async def get_time_machine_forecast(
228228
session=client_session,
229229
)
230230
return Forecast(**data)
231+
232+
async def get_recent_time_machine_forecast(
233+
self,
234+
latitude: float,
235+
longitude: float,
236+
time: datetime,
237+
client_session: aiohttp.ClientSession,
238+
extend: bool = None,
239+
lang=Languages.ENGLISH,
240+
values_units=Units.AUTO,
241+
exclude: [Weather] = None,
242+
timezone: str = None,
243+
) -> Forecast:
244+
required_time = int(time.timestamp())
245+
current_time = int(datetime.now().timestamp())
246+
if timezone:
247+
tz = pytz.timezone(timezone)
248+
current_time = int(datetime.now(tz).timestamp())
249+
250+
diff = required_time - current_time
251+
252+
exclude = self.convert_exclude_param_to_string(exclude)
253+
254+
url = self.get_url(latitude, longitude, diff)
255+
data = await self.request_manager.make_request(
256+
url=url,
257+
extend=Weather.HOURLY if extend else None,
258+
lang=lang,
259+
units=values_units,
260+
exclude=exclude,
261+
timezone=timezone,
262+
session=client_session,
263+
)
264+
return Forecast(**data)

pirate_weather/base.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,13 @@ class AutoInit:
3030
def __init__(self, **params):
3131
try:
3232
timezone = pytz.timezone(params.pop("timezone", None))
33-
except (pytz.UnknownTimeZoneError, AttributeError):
33+
except (pytz.UnknownTimeZoneError, AttributeError, TypeError):
3434
timezone = pytz.UTC
3535

36-
for field in self.__annotations__:
36+
for field in getattr(self.__class__, "__annotations__", {}):
3737
api_field = undo_snake_case_key(field)
38-
if self.__annotations__[field] == datetime:
38+
annotations = getattr(self.__class__, "__annotations__", {})
39+
if annotations.get(field) == datetime:
3940
params[api_field] = get_datetime_from_unix(
4041
params.get(api_field), timezone
4142
)

pirate_weather/forecast.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,22 @@ class CurrentlyForecast(base.AutoInit):
2424
uv_index: int
2525
visibility: float
2626
ozone: float
27+
# New fields from API v2+
28+
rain_intensity: float = None
29+
snow_intensity: float = None
30+
ice_intensity: float = None
31+
smoke: float = None
32+
solar: float = None
33+
feels_like: float = None
34+
cape: float = None
35+
fire_index: float = None
36+
liquid_accumulation: float = None
37+
snow_accumulation: float = None
38+
ice_accumulation: float = None
39+
station_pressure: float = None
40+
current_day_ice: float = None
41+
current_day_liquid: float = None
42+
current_day_snow: float = None
2743

2844

2945
class MinutelyForecastItem(base.AutoInit):
@@ -32,6 +48,10 @@ class MinutelyForecastItem(base.AutoInit):
3248
precip_intensity_error: float
3349
precip_probability: float
3450
precip_type: str
51+
# New fields from API v2+
52+
rain_intensity: float = None
53+
snow_intensity: float = None
54+
sleet_intensity: float = None
3555

3656

3757
class MinutelyForecast(base.BaseWeather):
@@ -44,6 +64,7 @@ class HourlyForecastItem(base.AutoInit):
4464
summary: str = None
4565
icon: str
4666
precip_intensity: float
67+
precip_intensity_error: float = None
4768
precip_probability: float
4869
precip_type: str
4970
precipAccumulation: float
@@ -59,6 +80,21 @@ class HourlyForecastItem(base.AutoInit):
5980
uv_index: int
6081
visibility: float
6182
ozone: float
83+
# New fields from API v2+
84+
nearest_storm_distance: int = None
85+
nearest_storm_bearing: int = None
86+
smoke: float = None
87+
solar: float = None
88+
feels_like: float = None
89+
cape: float = None
90+
fire_index: float = None
91+
liquid_accumulation: float = None
92+
snow_accumulation: float = None
93+
ice_accumulation: float = None
94+
rain_intensity: float = None
95+
snow_intensity: float = None
96+
ice_intensity: float = None
97+
station_pressure: float = None
6298

6399

64100
class HourlyForecast(base.BaseWeather):
@@ -107,13 +143,84 @@ class DailyForecastItem(base.AutoInit):
107143
apparent_temperature_min_time: int
108144
apparent_temperature_max: float
109145
apparent_temperature_max_time: int
146+
# New fields from API v2+
147+
rain_intensity: float = None
148+
rain_intensity_max: float = None
149+
rain_intensity_max_time: int = None
150+
snow_intensity: float = None
151+
snow_intensity_max: float = None
152+
snow_intensity_max_time: int = None
153+
ice_intensity: float = None
154+
ice_intensity_max: float = None
155+
ice_intensity_max_time: int = None
156+
smoke_max: float = None
157+
smoke_max_time: int = None
158+
solar_max: float = None
159+
solar_max_time: int = None
160+
cape_max: float = None
161+
cape_max_time: int = None
162+
fire_index_max: float = None
163+
fire_index_max_time: int = None
164+
liquid_accumulation: float = None
165+
snow_accumulation: float = None
166+
ice_accumulation: float = None
167+
current_day_ice: float = None
168+
current_day_liquid: float = None
169+
current_day_snow: float = None
170+
dawn_time: int = None
171+
dusk_time: int = None
110172

111173

112174
class DailyForecast(base.BaseWeather):
113175
data: list[DailyForecastItem]
114176
data_class = DailyForecastItem
115177

116178

179+
# DayNight block is similar to hourly but has some additional fields
180+
class DayNightForecastItem(base.AutoInit):
181+
time: int
182+
summary: str = None
183+
icon: str
184+
precip_intensity: float
185+
precip_intensity_max: float = None
186+
precip_probability: float
187+
precip_type: str
188+
precipAccumulation: float
189+
temperature: float
190+
apparent_temperature: float
191+
dew_point: float
192+
humidity: float
193+
pressure: float
194+
wind_speed: float
195+
wind_gust: float
196+
wind_bearing: int
197+
cloud_cover: float
198+
uv_index: int
199+
visibility: float
200+
ozone: float
201+
# Fields that may be in day_night
202+
smoke: float = None
203+
solar: float = None
204+
feels_like: float = None
205+
cape: float = None
206+
fire_index: float = None
207+
liquid_accumulation: float = None
208+
snow_accumulation: float = None
209+
ice_accumulation: float = None
210+
rain_intensity: float = None
211+
snow_intensity: float = None
212+
ice_intensity: float = None
213+
rain_intensity_max: float = None
214+
snow_intensity_max: float = None
215+
ice_intensity_max: float = None
216+
station_pressure: float = None
217+
218+
219+
class DayNightForecast(base.BaseWeather):
220+
data: list[DayNightForecastItem]
221+
data_class = DayNightForecastItem
222+
223+
117224
class Alert(base.AutoInit):
118225
title: str
119226
regions: list
@@ -130,6 +237,15 @@ class Flags(base.AutoInit):
130237
nearest__station: float
131238
pirate_weather__unavailable: bool
132239
units: str
240+
# New fields from API v2+
241+
source_times: dict = None
242+
source_i_d_x: dict = None
243+
version: str = None
244+
process_time: float = None
245+
ingest_version: str = None
246+
nearest_city: str = None
247+
nearest_country: str = None
248+
nearest_sub_national: str = None
133249

134250

135251
class Forecast:
@@ -140,6 +256,7 @@ class Forecast:
140256
minutely: MinutelyForecast
141257
hourly: HourlyForecast
142258
daily: DailyForecast
259+
day_night: DayNightForecast
143260
alerts: list[Alert]
144261
flags: Flags
145262
offset: int
@@ -153,6 +270,7 @@ def __init__(
153270
minutely: dict = None,
154271
hourly: dict = None,
155272
daily: dict = None,
273+
day_night: dict = None,
156274
alerts: [dict] = None,
157275
flags: dict = None,
158276
offset: int = None,
@@ -166,6 +284,7 @@ def __init__(
166284
self.minutely = MinutelyForecast(timezone=timezone, **(minutely or {}))
167285
self.hourly = HourlyForecast(timezone=timezone, **(hourly or {}))
168286
self.daily = DailyForecast(timezone=timezone, **(daily or {}))
287+
self.day_night = DayNightForecast(timezone=timezone, **(day_night or {}))
169288

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

0 commit comments

Comments
 (0)