Skip to content

Commit 106b652

Browse files
Merge pull request #58 from SheffieldSolar/56-update-capacity-methods-to-use-the-new-capacity-api-endpoints
Update capacity methods to use the new capacity endpoints form the PV_Live API
2 parents f63f6c9 + 73b6e7c commit 106b652

4 files changed

Lines changed: 32 additions & 91 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ A Python implementation of the PV_Live web API. See https://www.solar.sheffield.
1818

1919
## Usage
2020

21+
As of 2025-07-07, the production PV_Live API is hosted on Google Cloud Platform (GCP) at https://api.pvlive.uk. There is a also non-prod test/fix-on-fail (FOF) environment hosted on TUOS IT: https://api.solar.sheffield.ac.uk. To support switching between the two, the `pvlive-api` package exposes a parameter `domain_url`, which can be set to one of `["api.pvlive.uk", "api.solar.sheffield.ac.uk"]` but defaults to `api.pvlive.uk`.
22+
2123
There are three methods for extracting raw data from the PV_Live API:
2224

2325
|Method|Description|Docs Link|

Tests/test_pvlive_api.py

Lines changed: 0 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -133,25 +133,6 @@ def test_latest(self):
133133
)
134134
self.check_df_columns(data)
135135
self.check_df_dtypes(data)
136-
data = self.api.latest(
137-
entity_type="pes",
138-
entity_id=0,
139-
period=5
140-
)
141-
self.check_pes_tuple(data)
142-
self.check_pes_tuple_dtypes(data)
143-
data = self.api.latest(entity_type="pes", entity_id=0, period=5, dataframe=True)
144-
self.check_df_columns(data)
145-
self.check_df_dtypes(data)
146-
data = self.api.latest(
147-
entity_type="pes",
148-
entity_id=0,
149-
extra_fields="ucl_mw,lcl_mw,installedcapacity_mwp,stats_error",
150-
period=5,
151-
dataframe=True
152-
)
153-
self.check_df_columns(data)
154-
self.check_df_dtypes(data)
155136
data = self.api.latest(entity_type="gsp", entity_id=103)
156137
self.check_gsp_tuple(data)
157138
self.check_gsp_tuple_dtypes(data)
@@ -175,27 +156,6 @@ def test_day_peak(self):
175156
entity_id=0,
176157
dataframe=True
177158
)
178-
data = self.api.day_peak(d=test_date, entity_type="pes", entity_id=0, period=5)
179-
self.check_pes_tuple(data)
180-
self.check_pes_tuple_dtypes(data)
181-
data = self.api.day_peak(
182-
d=test_date,
183-
entity_type="pes",
184-
entity_id=0,
185-
period=5,
186-
dataframe=True
187-
)
188-
self.check_df_columns(data)
189-
self.check_df_dtypes(data)
190-
data = self.api.day_peak(
191-
d=test_date,
192-
extra_fields="ucl_mw,lcl_mw,installedcapacity_mwp,stats_error",
193-
entity_type="pes",
194-
entity_id=0,
195-
period=5,
196-
dataframe=True
197-
)
198-
self.check_df_dtypes(data)
199159
data = self.api.day_peak(d=test_date, entity_type="gsp", entity_id=54)
200160
self.check_gsp_tuple(data)
201161
self.check_gsp_tuple_dtypes(data)
@@ -235,25 +195,6 @@ def test_between(self):
235195
)
236196
self.check_df_columns(data)
237197
self.check_df_dtypes(data)
238-
data = self.api.between(
239-
start=get_test_time(0, 0),
240-
end=get_test_time(14, 0),
241-
entity_type="pes",
242-
entity_id=0,
243-
period=5
244-
)
245-
with self.subTest():
246-
assert isinstance(data, list)
247-
data = self.api.between(
248-
start=get_test_time(12, 20),
249-
end=get_test_time(14, 0),
250-
entity_type="pes",
251-
entity_id=0,
252-
period=5,
253-
dataframe=True
254-
)
255-
self.check_df_columns(data)
256-
self.check_df_dtypes(data)
257198

258199
def test_at_time(self):
259200
"""Test the at_time function."""
@@ -272,21 +213,6 @@ def test_at_time(self):
272213
dataframe=True
273214
)
274215
self.check_df_dtypes(data)
275-
data = self.api.at_time(dt=test_time, entity_type="pes", entity_id=0, period=5)
276-
self.check_pes_tuple(data)
277-
self.check_pes_tuple_dtypes(data)
278-
data = self.api.at_time(test_time, entity_type="pes", entity_id=0, period=5, dataframe=True)
279-
self.check_df_columns(data)
280-
self.check_df_dtypes(data)
281-
data = self.api.at_time(
282-
test_time,
283-
entity_type="pes",
284-
entity_id=0,
285-
extra_fields="ucl_mw,lcl_mw,installedcapacity_mwp,stats_error",
286-
period=5,
287-
dataframe=True
288-
)
289-
self.check_df_dtypes(data)
290216
data = self.api.at_time(dt=test_time, entity_type="gsp", entity_id=26)
291217
self.check_gsp_tuple(data)
292218
self.check_gsp_tuple_dtypes(data)

pvlive_api/pvlive.py

Lines changed: 29 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,26 @@ class PVLive:
4747
proxies : Optional[Dict]
4848
Optionally specify a Dict of proxies for http and https requests in the format:
4949
{"http": "<address>", "https": "<address>"}
50+
ssl_verify : Bool
51+
Set to False to disable SSL cert checks when querying the API. Useful if e.g. your company
52+
network/VPN has issues propagating SSL certs. Defaults to True. Not recommended to set to
53+
False!
54+
domain_url : Literal["api.pvlive.uk", "api.solar.sheffield.ac.uk", "api0.solar.sheffield.ac.uk"]
55+
Used to switch between the production PV_Live API on GCP and the test/FOF server on-prem.
56+
gsp_boundaries_version : str
57+
Some capacity datasets (by GSP) are encoded with the version of the GSP boundaries used.
5058
"""
5159
def __init__(
5260
self,
5361
retries: int = 3,
5462
proxies: Optional[Dict] = None,
5563
ssl_verify: bool = True,
5664
domain_url: Literal[
57-
"api0.solar.sheffield.ac.uk",
65+
"api.pvlive.uk",
5866
"api.solar.sheffield.ac.uk",
59-
"api.pvlive.uk"
60-
] = "api.solar.sheffield.ac.uk"
67+
"api0.solar.sheffield.ac.uk",
68+
] = "api.pvlive.uk",
69+
gsp_boundaries_version: str = "20250109",
6170
):
6271
valid_domain_urls = [
6372
"api0.solar.sheffield.ac.uk",
@@ -79,7 +88,9 @@ def __init__(
7988
self.pes_list = self._get_pes_list()
8089
self.gsp_ids = self.gsp_list.gsp_id.dropna().astype(int64).unique()
8190
self.pes_ids = self.pes_list.pes_id.dropna().astype(int64).unique()
91+
self.deployment_datasets = None
8292
self.deployment_releases = None
93+
self.gsp_boundaries_version = gsp_boundaries_version
8394

8495
def _get_gsp_list(self):
8596
"""Fetch the GSP list from the API and convert to Pandas DataFrame."""
@@ -94,15 +105,15 @@ def _get_pes_list(self):
94105
return pd.DataFrame(response["data"], columns=response["meta"])
95106

96107
def _get_deployment_releases(self):
97-
"""Get a list of deployment releases as datestamps (YYYYMMDD)."""
98-
if self.deployment_releases is None:
108+
"""
109+
Get the available deployment releases as a list of datestamps (YYYYMMDD) alongside a dict of
110+
files available in each release.
111+
"""
112+
if self.deployment_datasets is None:
99113
url = f"{self.domain_url}/capacity/"
100-
response = self._fetch_url(url, parse_json=False)
101-
soup = BeautifulSoup(response.content, "html.parser")
102-
releases = [r["href"].strip("/") for r in soup.find_all("a", href=True)
103-
if re.match(r"[0-9]{8}/", r["href"])]
104-
self.deployment_releases = sorted(releases, reverse=True)
105-
return self.deployment_releases
114+
self.deployment_datasets = self._fetch_url(url, parse_json=True)
115+
self.deployment_releases = sorted(list(self.deployment_datasets.keys()), reverse=True)
116+
return self.deployment_datasets, self.deployment_releases
106117

107118
def _get_deployment_filenames(self, release):
108119
"""Get a list of filenames for a given release."""
@@ -115,7 +126,7 @@ def _get_deployment_filenames(self, release):
115126

116127
def _validate_deployment_inputs(self, region, include_history, by_system_size, release):
117128
"""Validate input parameters to `deployment()`."""
118-
releases = self._get_deployment_releases()
129+
deployment_datasets, releases = self._get_deployment_releases()
119130
if not isinstance(region, str):
120131
raise TypeError("`region` must be a string.")
121132
supported_regions = ["gsp", "llsoa"]
@@ -165,22 +176,23 @@ def deployment(self,
165176
GSPs/llsoa and dc_capacity_mwp.
166177
"""
167178
self._validate_deployment_inputs(region, include_history, by_system_size, release)
168-
releases = self._get_deployment_releases()
179+
deployment_datasets, releases = self._get_deployment_releases()
169180
release = releases[release] if isinstance(release, int) else release
170-
filenames = self._get_deployment_filenames(release)
171-
region_ = "20220314_GSP" if region == "gsp" else region
181+
filenames = list(deployment_datasets[release].keys())
182+
region_ = f"{self.gsp_boundaries_version}_GSP" if region == "gsp" else region
172183
history_ = "_and_month" if include_history else ""
173184
system_size_ = "_and_system_size" if by_system_size else ""
174185
filename_ending = f"_capacity_by_{region_}{history_}{system_size_}.csv.gz"
175186
filename = [f for f in filenames if f.endswith(filename_ending)][0]
176-
url = f"{self.domain_url}/capacity/{release}/{filename}"
187+
url = deployment_datasets[release][filename]
177188
kwargs = dict(parse_dates=["install_month"]) if include_history else {}
178189
response = self._fetch_url(url, parse_json=False)
179190
mock_file = BytesIO(response.content)
180191
deployment_data = pd.read_csv(mock_file, compression={"method": "gzip"}, **kwargs)
181192
deployment_data.insert(0, "release", release)
182193
deployment_data.rename(columns={"dc_capacity_MWp": "dc_capacity_mwp"}, inplace=True)
183194
deployment_data.system_count = deployment_data.system_count.astype("Int64")
195+
deployment_data.dropna(how="any", inplace=True)
184196
return deployment_data
185197

186198
def latest(self,
@@ -532,7 +544,7 @@ def _validate_inputs(self, entity_type="gsp", entity_id=0, extra_fields="", peri
532544
elif entity_type == "gsp":
533545
if entity_id not in self.gsp_ids:
534546
raise PVLiveException(f"The gsp_id {entity_id} was not found.")
535-
periods = [5, 30]
547+
periods = [30]
536548
if period not in periods:
537549
raise ValueError("The period parameter must be one of: "
538550
f"{', '.join(map(str, periods))}.")

requirements_dev.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
sphinx
22
sphinx_rtd_theme
33
numpydoc
4+
pytest

0 commit comments

Comments
 (0)