diff --git a/docs/sphinx/source/whatsnew/v0.10.3.rst b/docs/sphinx/source/whatsnew/v0.10.3.rst index 007eb8d34b..b428e103c3 100644 --- a/docs/sphinx/source/whatsnew/v0.10.3.rst +++ b/docs/sphinx/source/whatsnew/v0.10.3.rst @@ -15,6 +15,8 @@ Enhancements Bug fixes ~~~~~~~~~ +* Fixed CAMS error message handler in + :py:func:`pvlib.iotools.get_cams` (:issue:`1799`, :pull:`1905`) * Fix mapping of the dew point column to ``temp_dew`` when ``map_variables`` is True in :py:func:`pvlib.iotools.get_psm3`. (:pull:`1920`) @@ -40,7 +42,8 @@ Contributors * Miguel Sánchez de León Peque (:ghuser:`Peque`) * Will Hobbs (:ghuser:`williamhobbs`) * Anton Driesse (:ghuser:`adriesse`) +* Gilles Fischer (:ghuser:`GillesFischerV`) +* Adam R. Jensen (:ghusuer:`AdamRJensen`) * :ghuser:`matsuobasho` * Harry Jack (:ghuser:`harry-solcast`) -* Adam R. Jensen (:ghuser:`AdamRJensen`) * Kevin Anderson (:ghuser:`kandersolar`) diff --git a/pvlib/iotools/sodapro.py b/pvlib/iotools/sodapro.py index 95d6ffd866..a5c0c351c8 100644 --- a/pvlib/iotools/sodapro.py +++ b/pvlib/iotools/sodapro.py @@ -57,8 +57,10 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear', Access: free, but requires registration, see [2]_ Requests: max. 100 per day + Geographical coverage: worldwide for CAMS McClear and approximately -66° to - 66° in both latitude and longitude for CAMS Radiation. + 66° in latitude and -66° to 180° in longitude for CAMS Radiation. See [3]_ + for a map of the geographical coverage. Parameters ---------- @@ -157,6 +159,9 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear', `_ .. [2] `CAMS Radiation Automatic Access (SoDa) `_ + .. [3] A. R. Jensen et al., pvlib iotools — Open-source Python functions + for seamless access to solar irradiance data. Solar Energy. 2023. Vol + 266, pp. 112092. :doi:`10.1016/j.solener.2023.112092` """ try: time_step_str = TIME_STEPS_MAP[time_step] @@ -215,14 +220,16 @@ def get_cams(latitude, longitude, start, end, email, identifier='mcclear', res = requests.get(base_url + '?DataInputs=' + data_inputs, params=params, timeout=timeout) - # Invalid requests returns an XML error message and the HTTP staus code 200 - # as if the request was successful. Therefore, errors cannot be handled - # automatic (e.g. res.raise_for_status()) and errors are handled manually - if res.headers['Content-Type'] == 'application/xml': + # Response from CAMS follows the status and reason format of PyWPS4 + # If an error occurs on server side, it will return error 400 - bad request + # Additional information is available in the response text, so it is added + # to the error displayed to facilitate users effort to fix their request + if not res.ok: errors = res.text.split('ows:ExceptionText')[1][1:-2] - raise requests.HTTPError(errors, response=res) + res.reason = "%s: <%s>" % (res.reason, errors) + res.raise_for_status() # Successful requests returns a csv data file - elif res.headers['Content-Type'] == 'application/csv': + else: fbuf = io.StringIO(res.content.decode('utf-8')) data, metadata = parse_cams(fbuf, integrated=integrated, label=label, map_variables=map_variables) diff --git a/pvlib/tests/iotools/test_sodapro.py b/pvlib/tests/iotools/test_sodapro.py index ff17691a98..4729cf2c64 100644 --- a/pvlib/tests/iotools/test_sodapro.py +++ b/pvlib/tests/iotools/test_sodapro.py @@ -248,7 +248,7 @@ def test_get_cams_bad_request(requests_mock): requests inputs. Also tests if the specified server url gets used""" # Subset of an xml file returned for errornous requests - mock_response_bad = """ + mock_response_bad_text = """ Failed to execute WPS process [get_mcclear]: Please, register yourself at www.soda-pro.com @@ -256,12 +256,13 @@ def test_get_cams_bad_request(requests_mock): url_cams_bad_request = 'https://pro.soda-is.com/service/wps?DataInputs=latitude=55.7906;longitude=12.5251;altitude=-999;date_begin=2020-01-01;date_end=2020-05-04;time_ref=TST;summarization=PT01H;username=test%2540test.com;verbose=false&Service=WPS&Request=Execute&Identifier=get_mcclear&version=1.0.0&RawDataOutput=irradiation' # noqa: E501 - requests_mock.get(url_cams_bad_request, text=mock_response_bad, - headers={'Content-Type': 'application/xml'}) + requests_mock.get(url_cams_bad_request, status_code=400, + text=mock_response_bad_text) # Test if HTTPError is raised if incorrect input is specified # In the below example a non-registrered email is specified - with pytest.raises(requests.HTTPError, match='Failed to execute WPS'): + with pytest.raises(requests.exceptions.HTTPError, + match='Failed to execute WPS process'): _ = sodapro.get_cams( start=pd.Timestamp('2020-01-01'), end=pd.Timestamp('2020-05-04'),