diff --git a/examples/energy.py b/examples/energy.py index cdc9db7..91c6c14 100644 --- a/examples/energy.py +++ b/examples/energy.py @@ -18,22 +18,23 @@ async def main() -> None: async def print_prices(client: SpotHinta, region: Region) -> None: """Print prices for the given region.""" - local = pytz.timezone("Europe/Helsinki") + pytz.timezone("Europe/Helsinki") energy_today = await client.energy_prices(region=region) utc_next_hour = energy_today.utcnow() + timedelta(hours=1) print(f"--- ENERGY TODAY FOR REGION {region.name}---") - print(f"Extremas price: {energy_today.extreme_prices}") + print(f"Lowest price today: {energy_today.lowest_price_today}") + print(f"Highest price today: {energy_today.highest_price_today}") print(f"Average price: {energy_today.average_price}") print() - highest_time = energy_today.highest_price_time.astimezone(local) + highest_time = energy_today.highest_price_time print(f"Highest price time: {highest_time}") - lowest_time = energy_today.lowest_price_time.astimezone(local) + lowest_time = energy_today.lowest_price_time print(f"Lowest price time: {lowest_time}") print() print(f"Current price: {energy_today.current_price}") - print(f"Next hourprice: {energy_today.price_at_time(utc_next_hour)}") + print(f"Next hour price: {energy_today.price_at_time(utc_next_hour)}") lower_hours: int = energy_today.hours_priced_equal_or_lower print(f"Lower hours: {lower_hours}") diff --git a/spothinta_api/models.py b/spothinta_api/models.py index 22c3b3e..779f5f3 100644 --- a/spothinta_api/models.py +++ b/spothinta_api/models.py @@ -32,7 +32,7 @@ def _timed_value(moment: datetime, prices: dict[datetime, float]) -> float | Non def _get_pricetime( prices: dict[datetime, float], func: Callable[[dict[datetime, float]], datetime], -) -> datetime: +) -> datetime | None: """Return the time of the price. Args: @@ -42,8 +42,11 @@ def _get_pricetime( Returns: ------- - The time of the price. + The time of the price or None if prices is None or empty. """ + if prices is None or len(prices) == 0: + return None + return func(prices, key=prices.get) # type: ignore[call-arg] @@ -59,32 +62,53 @@ def current_price(self) -> float | None: Returns ------- - The price for the current hour. + The price for the current hour or None if no price is available. """ return self.price_at_time(self.utcnow()) @property - def extreme_prices(self) -> tuple[float, float]: - """Return the minimum and maximum price. + def lowest_price_today(self) -> float | None: + """Return the minimum price today. Returns ------- - The minimum and maximum price. + The minimum price today or None if no prices are available for today. """ - return round(min(self.prices_today().values()), 5), round( - max(self.prices_today().values()), - 5, - ) + prices = self.prices_today() + + if len(prices) == 0: + return None + + return round(min(prices.values()), 5) @property - def average_price(self) -> float: - """Return the average price. + def highest_price_today(self) -> float | None: + """Return the maximum price today. Returns ------- - The average price. + The maximum price today or None if no prices are available for today. + """ + prices = self.prices_today() + + if len(prices) == 0: + return None + + return round(max(prices.values()), 5) + + @property + def average_price(self) -> float | None: + """Return the average price today. + + Returns + ------- + The average price today or None if no prices are available for today. """ prices_today = self.prices_today() + + if len(prices_today) == 0: + return None + return round(sum(prices_today.values()) / len(prices_today), 5) @property @@ -93,17 +117,17 @@ def highest_price_time(self) -> datetime: Returns ------- - The time of the highest price. + The time of the highest price or None if no prices are available for today. """ return _get_pricetime(self.prices_today(), max) @property - def lowest_price_time(self) -> datetime: + def lowest_price_time(self) -> datetime | None: """Return the time of the lowest price. Returns ------- - The time of the lowest price. + The time of the lowest price or None if no prices are available for today. """ return _get_pricetime(self.prices_today(), min) diff --git a/tests/fixtures/energy-only-tomorrow.json b/tests/fixtures/energy-only-tomorrow.json new file mode 100644 index 0000000..55dfadc --- /dev/null +++ b/tests/fixtures/energy-only-tomorrow.json @@ -0,0 +1,146 @@ +[ + { + "Rank": 22, + "DateTime": "2023-05-07T00:00:00+03:00", + "PriceNoTax": 0.0732, + "PriceWithTax": 0.0907 + }, + { + "Rank": 8, + "DateTime": "2023-05-07T01:00:00+03:00", + "PriceNoTax": 0.0614, + "PriceWithTax": 0.0761 + }, + { + "Rank": 10, + "DateTime": "2023-05-07T02:00:00+03:00", + "PriceNoTax": 0.0618, + "PriceWithTax": 0.0766 + }, + { + "Rank": 9, + "DateTime": "2023-05-07T03:00:00+03:00", + "PriceNoTax": 0.0617, + "PriceWithTax": 0.0765 + }, + { + "Rank": 11, + "DateTime": "2023-05-07T04:00:00+03:00", + "PriceNoTax": 0.062, + "PriceWithTax": 0.0769 + }, + { + "Rank": 12, + "DateTime": "2023-05-07T05:00:00+03:00", + "PriceNoTax": 0.0637, + "PriceWithTax": 0.079 + }, + { + "Rank": 13, + "DateTime": "2023-05-07T06:00:00+03:00", + "PriceNoTax": 0.0645, + "PriceWithTax": 0.08 + }, + { + "Rank": 14, + "DateTime": "2023-05-07T07:00:00+03:00", + "PriceNoTax": 0.066, + "PriceWithTax": 0.0819 + }, + { + "Rank": 24, + "DateTime": "2023-05-07T08:00:00+03:00", + "PriceNoTax": 0.0784, + "PriceWithTax": 0.0972 + }, + { + "Rank": 23, + "DateTime": "2023-05-07T09:00:00+03:00", + "PriceNoTax": 0.0759, + "PriceWithTax": 0.0941 + }, + { + "Rank": 17, + "DateTime": "2023-05-07T10:00:00+03:00", + "PriceNoTax": 0.0676, + "PriceWithTax": 0.0838 + }, + { + "Rank": 5, + "DateTime": "2023-05-07T11:00:00+03:00", + "PriceNoTax": 0.0374, + "PriceWithTax": 0.0464 + }, + { + "Rank": 2, + "DateTime": "2023-05-07T12:00:00+03:00", + "PriceNoTax": 0.0305, + "PriceWithTax": 0.0378 + }, + { + "Rank": 6, + "DateTime": "2023-05-07T13:00:00+03:00", + "PriceNoTax": 0.0393, + "PriceWithTax": 0.0487 + }, + { + "Rank": 4, + "DateTime": "2023-05-07T14:00:00+03:00", + "PriceNoTax": 0.0351, + "PriceWithTax": 0.0435 + }, + { + "Rank": 3, + "DateTime": "2023-05-07T15:00:00+03:00", + "PriceNoTax": 0.0307, + "PriceWithTax": 0.038 + }, + { + "Rank": 1, + "DateTime": "2023-05-07T16:00:00+03:00", + "PriceNoTax": 0.0301, + "PriceWithTax": 0.0373 + }, + { + "Rank": 7, + "DateTime": "2023-05-07T17:00:00+03:00", + "PriceNoTax": 0.0566, + "PriceWithTax": 0.0702 + }, + { + "Rank": 19, + "DateTime": "2023-05-07T18:00:00+03:00", + "PriceNoTax": 0.0689, + "PriceWithTax": 0.0854 + }, + { + "Rank": 20, + "DateTime": "2023-05-07T19:00:00+03:00", + "PriceNoTax": 0.0722, + "PriceWithTax": 0.0895 + }, + { + "Rank": 21, + "DateTime": "2023-05-07T20:00:00+03:00", + "PriceNoTax": 0.0723, + "PriceWithTax": 0.0896 + }, + { + "Rank": 18, + "DateTime": "2023-05-07T21:00:00+03:00", + "PriceNoTax": 0.0679, + "PriceWithTax": 0.0842 + }, + { + "Rank": 15, + "DateTime": "2023-05-07T22:00:00+03:00", + "PriceNoTax": 0.066, + "PriceWithTax": 0.0819 + }, + { + "Rank": 16, + "DateTime": "2023-05-07T23:00:00+03:00", + "PriceNoTax": 0.0668, + "PriceWithTax": 0.0828 + } +] diff --git a/tests/test_models.py b/tests/test_models.py index 9b1b73f..e0ae4fb 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -11,8 +11,8 @@ @pytest.mark.freeze_time("2023-05-06 15:00:00+03:00") -async def test_electricity_model(aresponses: ResponsesMockServer) -> None: - """Test the electricity model for usage at 15:00:00 UTC+3.""" +async def test_model(aresponses: ResponsesMockServer) -> None: + """Test the model for usage at 15:00:00 UTC+3.""" aresponses.add( "api.spot-hinta.fi", "/TodayAndDayForward", @@ -28,8 +28,8 @@ async def test_electricity_model(aresponses: ResponsesMockServer) -> None: energy: Electricity = await client.energy_prices() assert energy is not None assert isinstance(energy, Electricity) - assert energy.extreme_prices[1] == 0.1055 - assert energy.extreme_prices[0] == 0.062 + assert energy.highest_price_today == 0.1055 + assert energy.lowest_price_today == 0.062 assert energy.average_price == 0.08935 assert energy.current_price == 0.062 assert energy.hours_priced_equal_or_lower == 1 @@ -48,8 +48,8 @@ async def test_electricity_model(aresponses: ResponsesMockServer) -> None: @pytest.mark.freeze_time("2023-05-06 15:00:00+03:00") -async def test_electricity_midnight(aresponses: ResponsesMockServer) -> None: - """Test the electricity model between 00:00 and 01:00 in UTC+3.""" +async def test_midnight(aresponses: ResponsesMockServer) -> None: + """Test the model between 00:00 and 01:00 in UTC+3.""" aresponses.add( "api.spot-hinta.fi", "/TodayAndDayForward", @@ -67,7 +67,7 @@ async def test_electricity_midnight(aresponses: ResponsesMockServer) -> None: assert energy.current_price == 0.062 -async def test_electricity_none_data(aresponses: ResponsesMockServer) -> None: +async def test_none_data(aresponses: ResponsesMockServer) -> None: """Test when there is no data for the current datetime.""" aresponses.add( "api.spot-hinta.fi", @@ -87,6 +87,37 @@ async def test_electricity_none_data(aresponses: ResponsesMockServer) -> None: assert energy.current_price is None +@pytest.mark.freeze_time("2023-05-06 19:01:32+03:00") +async def test_only_data_for_tomorrow(aresponses: ResponsesMockServer) -> None: + """Test when there is only data for tomorrow.""" + aresponses.add( + "api.spot-hinta.fi", + "/TodayAndDayForward", + "GET", + aresponses.Response( + status=200, + headers={"Content-Type": "application/json"}, + text=load_fixtures("energy-only-tomorrow.json"), + ), + ) + async with ClientSession() as session: + client = SpotHinta(session=session) + energy: Electricity = await client.energy_prices() + assert energy is not None + assert isinstance(energy, Electricity) + assert energy.current_price is None + assert energy.average_price is None + assert energy.highest_price_today is None + assert energy.lowest_price_today is None + assert energy.hours_priced_equal_or_lower == 0 + # The price for another hour + another_hour = datetime(2023, 5, 6, 17, 0, tzinfo=timezone.utc) + assert energy.price_at_time(another_hour) is None + assert energy.lowest_price_time is None + assert energy.highest_price_time is None + assert isinstance(energy.timestamp_prices, list) + + async def test_no_electricity_data(aresponses: ResponsesMockServer) -> None: """Test when there is no electricity data.""" aresponses.add(