Initial commit: weather-or-not scaffold

This commit is contained in:
2026-04-29 04:46:01 +00:00
commit 86e6499895
13 changed files with 1353 additions and 0 deletions

0
tests/__init__.py Normal file
View File

184
tests/test_activities.py Normal file
View File

@@ -0,0 +1,184 @@
"""Tests for activity evaluators."""
from __future__ import annotations
import pytest
from datetime import date, timedelta
from pathlib import Path
from unittest.mock import AsyncMock, MagicMock, patch
from weather_or_not.activities import DataStore, Verdict
from weather_or_not.motorcycle import MotorcycleActivity
from weather_or_not.lawn import LawnActivity
from weather_or_not.weather import WeatherForecast
def _make_weather(
temp: float = 20,
wind: float = 10,
humidity: float = 50,
weather_code: int = 0,
precip_today: float = 0,
precip_24h: float = 0,
precip_2h: float = 0,
) -> WeatherForecast:
"""Create a minimal WeatherForecast for testing."""
return WeatherForecast(
latitude=40.7128,
longitude=-74.0060,
generationtime_ms=10.0,
utc_offset_seconds=-14400,
timezone="America/New_York",
timezone_abbreviation="EDT",
elevation=10.0,
current={
"temperature_4m": temp,
"wind_speed_10m": wind,
"relative_humidity_2m": humidity,
"weather_code": weather_code,
"precipitation": 0,
"is_day": 1,
"pressure_msl": 1013,
},
hourly={
"time": [
"2026-04-29T10:00:00",
"2026-04-29T11:00:00",
"2026-04-29T12:00:00",
],
"temperature_2m": [temp, temp + 1, temp + 2],
"relative_humidity_2m": [humidity, humidity - 5, humidity - 10],
"precipitation": [0, 0, 0],
"wind_speed_10m": [wind, wind - 1, wind - 2],
"weather_code": [0, 0, 0],
"apparent_temperature": [temp, temp + 1, temp + 2],
},
daily={
"time": ["2026-04-29"],
"precipitation_sum": [precip_today],
"temperature_2m_max": [temp + 5],
"temperature_2m_min": [temp - 5],
},
)
class TestDataStore:
def test_never_done(self, tmp_path):
store = DataStore(tmp_path)
assert store.days_since_activity("mow_lawn") == 999
def test_record_and_check(self, tmp_path):
store = DataStore(tmp_path)
store.record_activity_performed("mow_lawn", date(2026, 4, 25))
assert store.days_since_activity("mow_lawn") == 4 # Apr 29 - Apr 25
def test_stats(self, tmp_path):
store = DataStore(tmp_path)
store.record_activity_performed("mow_lawn", date(2026, 4, 25))
stats = store.get_activity_stats("mow_lawn")
assert stats["times_performed"] == 1
assert stats["last_performed"] == "2026-04-25"
def test_persistence(self, tmp_path):
"""Data persists across DataStore instances."""
store1 = DataStore(tmp_path)
store1.record_activity_performed("mow_lawn", date(2026, 4, 20))
del store1
store2 = DataStore(tmp_path)
assert store2.last_activity_date("mow_lawn") == date(2026, 4, 20)
class TestMotorcycleActivity:
def setup_method(self):
self.config = {
"min_temp_c": 10,
"max_temp_c": 35,
"max_precipitation_mm": 0,
"max_wind_speed_kmh": 30,
"max_humidity_pct": 90,
"no_ice": True,
"no_wet_roads": True,
}
self.data_store = DataStore(Path("/tmp/test-won-data"))
self.activity = MotorcycleActivity(self.config, self.data_store)
@pytest.mark.asyncio
async def test_good_conditions(self):
weather = _make_weather(temp=22, wind=10, humidity=50, weather_code=0)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.GO
assert all(r.passed for r in result.rules)
@pytest.mark.asyncio
async def test_rain(self):
weather = _make_weather(precip_today=5)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.NO_GO
assert any(not r.passed and "precip" in r.rule_name.lower()
for r in result.rules)
@pytest.mark.asyncio
async def test_wind_too_strong(self):
weather = _make_weather(wind=40)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.NO_GO
assert any(not r.passed and "wind" in r.rule_name.lower()
for r in result.rules)
@pytest.mark.asyncio
async def test_temp_too_cold(self):
weather = _make_weather(temp=5)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.NO_GO
@pytest.mark.asyncio
async def test_thunderstorm(self):
weather = _make_weather(weather_code=95)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.NO_GO
class TestLawnActivity:
def setup_method(self):
self.config = {
"cooldown_days": 3,
"max_precipitation_today_mm": 0,
"max_precipitation_past_24h_mm": 5,
"max_wind_speed_kmh": 25,
"min_temp_c": 15,
}
self.data_store = DataStore(Path("/tmp/test-lawn-data"))
self.activity = LawnActivity(self.config, self.data_store)
@pytest.mark.asyncio
async def test_good_conditions(self):
# Ensure cooldown is met
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20))
weather = _make_weather(temp=22, wind=10)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.GO
@pytest.mark.asyncio
async def test_cooldown_not_met(self):
# Just mowed yesterday
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 28))
weather = _make_weather(temp=22, wind=10)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.NO_GO
assert any(not r.passed and "cooldown" in r.rule_name.lower()
for r in result.rules)
@pytest.mark.asyncio
async def test_ground_wet(self):
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20))
weather = _make_weather(precip_24h=10)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.NO_GO
@pytest.mark.asyncio
async def test_rain_today(self):
self.data_store.record_activity_performed("mow_lawn", date(2026, 4, 20))
weather = _make_weather(precip_today=3)
result = await self.activity.evaluate(weather)
assert result.verdict == Verdict.NO_GO