From f0fc08fdaa5a4195b0b45fd21420d213362c411c Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Mon, 3 Nov 2025 15:38:20 +0000 Subject: [PATCH 1/2] Validate the API response includes the required keys (data and meta) --- pvlive_api/pvlive.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pvlive_api/pvlive.py b/pvlive_api/pvlive.py index 562cc34..2db1956 100644 --- a/pvlive_api/pvlive.py +++ b/pvlive_api/pvlive.py @@ -96,12 +96,14 @@ def _get_gsp_list(self): """Fetch the GSP list from the API and convert to Pandas DataFrame.""" url = f"{self.base_url}/gsp_list" response = self._fetch_url(url) + self._validate_api_response(response, expected_keys=("data", "meta")) return pd.DataFrame(response["data"], columns=response["meta"]) def _get_pes_list(self): """Fetch the PES list from the API and convert to Pandas DataFrame.""" url = f"{self.base_url}/pes_list" response = self._fetch_url(url) + self._validate_api_response(response, expected_keys=("data", "meta")) return pd.DataFrame(response["data"], columns=response["meta"]) def _get_deployment_releases(self): @@ -239,6 +241,7 @@ def latest(self, extra_fields=extra_fields, period=period) params = self._compile_params(extra_fields, period=period) response = self._query_api(entity_type, entity_id, params) + self._validate_api_response(response, expected_keys=("data", "meta")) if response["data"]: data, meta = response["data"], response["meta"] data = tuple(data[0]) @@ -455,12 +458,22 @@ def _between(self, start, end, entity_type="gsp", entity_id=0, extra_fields="", request_end = min(end, request_start + max_range) params = self._compile_params(extra_fields, request_start, request_end, period) response = self._query_api(entity_type, entity_id, params) + self._validate_api_response(response, expected_keys=("data", "meta")) data += response["data"] request_start += max_range + timedelta(minutes=period) if dataframe: return self._convert_tuple_to_df(data, response["meta"]), response["meta"] return data, response["meta"] + @staticmethod + def _validate_api_response(response, expected_keys): + """Check that a JSON API response contains the expected keys.""" + if any(key not in response.keys() for key in expected_keys): + raise PVLiveException( + "The API's JSON response did not contain the required fields. Expected keys: " + f"{expected_keys} , available keys: {response.keys()}" + ) + def _compile_params(self, extra_fields="", start=None, end=None, period=30): """Compile parameters into a Python dict, formatting where necessary.""" params = {} From 4509b248ba6a105e34f9ded82c96a8038ebc0310 Mon Sep 17 00:00:00 2001 From: Jamie Taylor Date: Mon, 3 Nov 2025 15:38:41 +0000 Subject: [PATCH 2/2] Update unit tests to check the validation works --- Tests/test_pvlive_api.py | 6 +++++- pvlive_api/__init__.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/Tests/test_pvlive_api.py b/Tests/test_pvlive_api.py index cff3ff1..b6323a0 100644 --- a/Tests/test_pvlive_api.py +++ b/Tests/test_pvlive_api.py @@ -5,11 +5,12 @@ """ import unittest +from unittest.mock import MagicMock from datetime import datetime, date, time import pytz import pandas.api.types as ptypes -from pvlive_api import PVLive +from pvlive_api import PVLive, PVLiveException class PVLiveTestCase(unittest.TestCase): """Tests for `pvlive.py`.""" @@ -139,6 +140,9 @@ def test_latest(self): data = self.api.latest(entity_type="gsp", entity_id=103, dataframe=True) self.check_df_columns(data) self.check_df_dtypes(data) + self.api._fetch_url = MagicMock(return_value={"notdata": [], "notmeta": []}) + with self.assertRaises(PVLiveException): + data = self.api.latest(entity_type="gsp", entity_id=0, dataframe=True) def test_day_peak(self): """Tests the day_peak function.""" diff --git a/pvlive_api/__init__.py b/pvlive_api/__init__.py index fdadbf1..b7bd0fa 100644 --- a/pvlive_api/__init__.py +++ b/pvlive_api/__init__.py @@ -1,3 +1,3 @@ -from pvlive_api.pvlive import PVLive +from pvlive_api.pvlive import PVLive, PVLiveException -__all__ = ["PVLive"] +__all__ = ["PVLive", "PVLiveException"]