diff --git a/src/smhi/mesan.py b/src/smhi/mesan.py index 72b41a93..02be6464 100644 --- a/src/smhi/mesan.py +++ b/src/smhi/mesan.py @@ -71,7 +71,7 @@ def parameters(self) -> Parameters: Returns: parameter model """ - return self._parameters.parameter + return self._parameters def _get_parameters(self) -> Parameters: """Get parameters from SMHI. @@ -140,8 +140,9 @@ def get_geo_multipoint(self, downsample: int = 2) -> Polygon: multipoint polygon model """ downsample = self._check_downsample(downsample) - multipoint_url = f"geotype/multipoint.json?downsample={downsample}" - data, headers, status = self._get_data(self._base_url + multipoint_url) + data, headers, status = self._get_data( + self._base_url + f"geotype/multipoint.json?downsample={downsample}" + ) return self.__polygon_model( status=status, @@ -164,8 +165,9 @@ def get_point( Returns: point data model """ - url = self._build_point_url(longitude, latitude) - data, headers, status = self._get_data(url) + data, headers, status = self._get_data( + self._base_url + f"geotype/point/lon/{longitude}/lat/{latitude}/data.json" + ) data_table, info_table = self._format_data_point(data) return self.__point_data_model( @@ -314,18 +316,6 @@ def _format_data_multipoint(self, data: dict) -> Optional[pd.DataFrame]: return pd.DataFrame(formatted_data) - def _build_point_url(self, lon: float, lat: float) -> str: - """Build point url. - - Args: - lon: longitude - lat: latitude - - Returns: - valid point url - """ - return self._base_url + f"geotype/point/lon/{lon}/lat/{lat}/data.json" - def _build_multipoint_url( self, validtime: str, @@ -382,4 +372,4 @@ def _check_valid_time(self, test_time: str) -> bool: true if valid and false if not valid """ valid_time = self._format_datetime(test_time) - return -1 < (arrow.now().shift(hours=-1) - arrow.get(valid_time)).days < 1 + return -1 < (arrow.now("Z").shift(hours=-1) - arrow.get(valid_time)).days < 1 diff --git a/src/smhi/metfcts.py b/src/smhi/metfcts.py index 03cc9e47..f090efd4 100644 --- a/src/smhi/metfcts.py +++ b/src/smhi/metfcts.py @@ -50,4 +50,4 @@ def _check_valid_time(self, test_time: str) -> bool: true if valid and false if not valid """ valid_time = self._format_datetime(test_time) - return -1 < (arrow.get(valid_time) - arrow.now()).days < 10 + return -1 < (arrow.get(valid_time) - arrow.now("Z").shift(hours=-1)).days < 10 diff --git a/src/smhi/models/mesan_model.py b/src/smhi/models/mesan_model.py index c6282e6a..670eebdb 100644 --- a/src/smhi/models/mesan_model.py +++ b/src/smhi/models/mesan_model.py @@ -60,6 +60,11 @@ class MesanMultiPointDataSchema(pa.DataFrameModel): value: Series[float] +class MesanGeometry(BaseModel): + type_: str = Field(..., alias="type") + coordinates: List[List[float]] + + class MesanPointData(BaseModel): model_config = ConfigDict(arbitrary_types_allowed=True) @@ -68,7 +73,7 @@ class MesanPointData(BaseModel): approved_time: str reference_time: str level_unit: str - + geometry: MesanGeometry df: pd.DataFrame df_info: DataFrame[MesanPointDataInfoSchema] diff --git a/tests/fixtures/mesan/multipoint.txt b/tests/fixtures/mesan/multipoint.txt new file mode 100644 index 00000000..c2354a90 --- /dev/null +++ b/tests/fixtures/mesan/multipoint.txt @@ -0,0 +1,18 @@ +Accept-Ranges: bytes +Access-Control-Allow-Headers: X-Requested-With, Content-Type +Access-Control-Allow-Origin: * +Age: 0 +Cache-Control: max-age=3600,public +Connection: keep-alive +Content-Encoding: gzip +Content-Type: application/json +Date: Sun, 31 Mar 2024 09:31:35 GMT +Expires: Sun, 31 Mar 2024 10:31:35 GMT +Last-Modified: Sun, 31 Mar 2024 09:31:35 GMT +Transfer-Encoding: chunked +Vary: Accept-Encoding +Via: 1.1 varnish (Varnish/6.6) +X-Varnish: 681874038 +X-hits: 0 + +{"approvedTime":"2024-03-31T08:39:25Z","referenceTime":"2024-03-31T08:00:00Z","geometry":{"type":"MultiPoint","coordinates":[[2.250475,52.500440]]},"timeSeries":[{"validTime":"2024-03-31T08:00:00Z","parameters":[{"name":"t","levelType":"hl","level":2,"unit":"Cel","values":[8.3]}]}]} \ No newline at end of file diff --git a/tests/fixtures/mesan/multipoint_data.csv b/tests/fixtures/mesan/multipoint_data.csv new file mode 100644 index 00000000..ed4068dc --- /dev/null +++ b/tests/fixtures/mesan/multipoint_data.csv @@ -0,0 +1,2 @@ +,value,lat,lon +0,8.3,52.50044,2.250475 diff --git a/tests/fixtures/mesan/point.txt b/tests/fixtures/mesan/point.txt new file mode 100644 index 00000000..50d7aaf7 --- /dev/null +++ b/tests/fixtures/mesan/point.txt @@ -0,0 +1,19 @@ +HTTP/1.1 200 OK +Date: Sun, 31 Mar 2024 09:24:00 GMT +Expires: Sun, 31 Mar 2024 10:24:00 GMT +Access-Control-Allow-Headers: X-Requested-With, Content-Type +Cache-Control: max-age=3600,public +Content-Type: application/json +Content-Encoding: gzip +Vary: Accept-Encoding +X-Varnish: 657956746 +Age: 0 +Via: 1.1 varnish (Varnish/6.6) +X-hits: 0 +Access-Control-Allow-Origin: * +Last-Modified: Sun, 31 Mar 2024 09:24:00 GMT +Accept-Ranges: bytes +Connection: keep-alive +Transfer-Encoding: chunked + +{"approvedTime":"2024-03-31T08:39:25Z","referenceTime":"2024-03-31T08:00:00Z","geometry":{"type":"Point","coordinates":[[16.150350,58.570784]]},"timeSeries":[{"validTime":"2024-03-31T08:00:00Z","parameters":[{"name":"t","levelType":"hl","level":2,"unit":"Cel","values":[5.5]},{"name":"gust","levelType":"hl","level":10,"unit":"m/s","values":[5.7]},{"name":"r","levelType":"hl","level":2,"unit":"percent","values":[90]},{"name":"msl","levelType":"hmsl","level":0,"unit":"hPa","values":[1002.5]},{"name":"Tiw","levelType":"hl","level":2,"unit":"Cel","values":[4.8]},{"name":"cb_sig","levelType":"hmsl","level":0,"unit":"m","values":[178]},{"name":"cb_sig","levelType":"hl","level":0,"unit":"m","values":[143]},{"name":"c_sigfr","levelType":"hl","level":0,"unit":"percent","values":[80]},{"name":"tcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"ct_sig","levelType":"hl","level":0,"unit":"m","values":[9792]},{"name":"lcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"hcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"mcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"prtype","levelType":"hl","level":0,"unit":"code","values":[-9]},{"name":"pmax","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"pmin","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"pmedian","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"pmean","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"prec1h","levelType":"hl","level":0,"unit":"mm","values":[0.0]},{"name":"prec3h","levelType":"hl","level":0,"unit":"mm","values":[0.0]},{"name":"frsn1h","levelType":"hl","level":0,"unit":"cm","values":[0.0]},{"name":"vis","levelType":"hl","level":2,"unit":"km","values":[43.9]},{"name":"spp","levelType":"hl","level":0,"unit":"percent","values":[-9]},{"name":"prsort","levelType":"hl","level":0,"unit":"code","values":[0]},{"name":"wd","levelType":"hl","level":10,"unit":"degree","values":[86]},{"name":"ws","levelType":"hl","level":10,"unit":"m/s","values":[2.9]},{"name":"Wsymb2","levelType":"hl","level":0,"unit":"category","values":[6]}]},{"validTime":"2024-03-31T07:00:00Z","parameters":[{"name":"t","levelType":"hl","level":2,"unit":"Cel","values":[4.7]},{"name":"gust","levelType":"hl","level":10,"unit":"m/s","values":[5.1]},{"name":"r","levelType":"hl","level":2,"unit":"percent","values":[93]},{"name":"msl","levelType":"hmsl","level":0,"unit":"hPa","values":[1002.6]},{"name":"Tiw","levelType":"hl","level":2,"unit":"Cel","values":[4.2]},{"name":"cb_sig","levelType":"hmsl","level":0,"unit":"m","values":[178]},{"name":"cb_sig","levelType":"hl","level":0,"unit":"m","values":[139]},{"name":"c_sigfr","levelType":"hl","level":0,"unit":"percent","values":[80]},{"name":"tcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"ct_sig","levelType":"hl","level":0,"unit":"m","values":[6728]},{"name":"lcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"hcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"mcc","levelType":"hl","level":0,"unit":"octas","values":[8]},{"name":"prtype","levelType":"hl","level":0,"unit":"code","values":[-9]},{"name":"pmax","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"pmin","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"pmedian","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"pmean","levelType":"hl","level":0,"unit":"kg/m2/h","values":[0.0]},{"name":"prec1h","levelType":"hl","level":0,"unit":"mm","values":[0.0]},{"name":"prec3h","levelType":"hl","level":0,"unit":"mm","values":[0.0]},{"name":"frsn1h","levelType":"hl","level":0,"unit":"cm","values":[0.0]},{"name":"vis","levelType":"hl","level":2,"unit":"km","values":[38.0]},{"name":"spp","levelType":"hl","level":0,"unit":"percent","values":[-9]},{"name":"prsort","levelType":"hl","level":0,"unit":"code","values":[0]},{"name":"wd","levelType":"hl","level":10,"unit":"degree","values":[83]},{"name":"ws","levelType":"hl","level":10,"unit":"m/s","values":[2.4]},{"name":"Wsymb2","levelType":"hl","level":0,"unit":"category","values":[6]}]}]} \ No newline at end of file diff --git a/tests/fixtures/mesan/point_data.csv b/tests/fixtures/mesan/point_data.csv new file mode 100644 index 00000000..a0dae9e2 --- /dev/null +++ b/tests/fixtures/mesan/point_data.csv @@ -0,0 +1,3 @@ +valid_time,Tiw,Wsymb2,c_sigfr,cb_sig,ct_sig,frsn1h,gust,hcc,lcc,mcc,msl,pmax,pmean,pmedian,pmin,prec1h,prec3h,prsort,prtype,r,spp,t,tcc,vis,wd,ws +2024-03-31 07:00:00+00:00,4.2,6,80,178,6728,0.0,5.1,8,8,8,1002.6,0.0,0.0,0.0,0.0,0.0,0.0,0,-9,93,-9,4.7,8,38.0,83,2.4 +2024-03-31 08:00:00+00:00,4.8,6,80,178,9792,0.0,5.7,8,8,8,1002.5,0.0,0.0,0.0,0.0,0.0,0.0,0,-9,90,-9,5.5,8,43.9,86,2.9 diff --git a/tests/fixtures/mesan/point_data_info.csv b/tests/fixtures/mesan/point_data_info.csv new file mode 100644 index 00000000..c3d04466 --- /dev/null +++ b/tests/fixtures/mesan/point_data_info.csv @@ -0,0 +1,27 @@ +name,level,level_type,unit +Tiw,2,hl,Cel +Wsymb2,0,hl,category +c_sigfr,0,hl,percent +cb_sig,0,hmsl,m +ct_sig,0,hl,m +frsn1h,0,hl,cm +gust,10,hl,m/s +hcc,0,hl,octas +lcc,0,hl,octas +mcc,0,hl,octas +msl,0,hmsl,hPa +pmax,0,hl,kg/m2/h +pmean,0,hl,kg/m2/h +pmedian,0,hl,kg/m2/h +pmin,0,hl,kg/m2/h +prec1h,0,hl,mm +prec3h,0,hl,mm +prsort,0,hl,code +prtype,0,hl,code +r,2,hl,percent +spp,0,hl,percent +t,2,hl,Cel +tcc,0,hl,octas +vis,2,hl,km +wd,10,hl,degree +ws,10,hl,m/s diff --git a/tests/unit/test_unit_mesan.py b/tests/unit/test_unit_mesan.py index 685d575b..5e864df2 100644 --- a/tests/unit/test_unit_mesan.py +++ b/tests/unit/test_unit_mesan.py @@ -6,253 +6,456 @@ import arrow import pandas as pd import pytest +from pydantic import BaseModel, ConfigDict +from smhi.constants import MESAN_LEVELS_UNIT, MESAN_PARAMETER_DESCRIPTIONS from smhi.mesan import Mesan +from smhi.models.mesan_model import MesanGeometry, MesanParameterItem, MesanParameters BASE_URL = ( "https://opendata-download-metanalys.smhi.se/" + "api/category/mesan2g/version/1/" ) +MOCK_MESAN_PARAMETERS = MesanParameters( + status=200, + headers={"Date": "Sun, 31 Mar 2024 07:37:51 GMT"}, + parameter=[ + MesanParameterItem( + name="t", + key="t_hl_2", + levelType="hl", + level=2, + unit="Cel", + missingValue=9999, + ), + MesanParameterItem( + name="gust", + key="gust_hl_10", + levelType="hl", + level=10, + unit="m/s", + missingValue=9999, + ), + ], +) + + +class MockResponse: + def __init__(self, status, header, content): + self.status_code = status + self.headers = header + self.content = content + + +class MockMesanPointData(BaseModel): + model_config = ConfigDict(arbitrary_types_allowed=True) + + approved_time: str + reference_time: str + level_unit: str + + +class MockMesanMultiPointData(BaseModel): + parameter: str + approved_time: str + reference_time: str + valid_time: str + + +def get_response(file, encode=False): + """Read in response. + + Args: + file: file to load + + Returns: + mocked response + """ + with open(file) as f: + mocked_response = f.read() + + headers, content = mocked_response.split("\n\n") + status = 200 + headers = {x.split(":")[0]: x.split(":")[1] for x in headers.split("\n")[1:]} + + if encode is True: + content = content.encode("utf-8") + + mocked_get = MockResponse(status, headers, content) + + return mocked_get + + +def get_data(file, load_type=None): + """Read in expected data structure. + + Args: + file: file to load + model + + Returns: + expected pydantic model + """ + with open(file) as f: + if load_type == "data": + return f.read().encode("utf-8") + + return json.load(f) + + +@pytest.fixture +def setup_point(): + """Read in Point response.""" + mocked_response = get_response("tests/fixtures/mesan/point.txt") + mocked_model = json.loads(mocked_response.content) + + mocked_approved_time = mocked_model["approvedTime"] + mocked_reference_time = mocked_model["referenceTime"] + mocked_geometry = MesanGeometry( + type=mocked_model["geometry"]["type"], + coordinates=mocked_model["geometry"]["coordinates"], + ) + mocked_data = pd.read_csv( + "tests/fixtures/mesan/point_data.csv", index_col="valid_time" + ) + mocked_data.index = pd.to_datetime(mocked_data.index) + mocked_data.columns.name = "name" + mocked_data_info = pd.read_csv( + "tests/fixtures/mesan/point_data_info.csv", index_col="name" + ) + + return ( + mocked_response, + mocked_approved_time, + mocked_reference_time, + mocked_geometry, + mocked_data, + mocked_data_info, + ) + + +@pytest.fixture +def setup_multipoint(): + """Read in MultiPoint response.""" + mocked_response = get_response("tests/fixtures/mesan/multipoint.txt") + mocked_model = json.loads(mocked_response.content) + + mocked_approved_time = mocked_model["approvedTime"] + mocked_reference_time = mocked_model["referenceTime"] + mocked_valid_time = mocked_model["timeSeries"][0]["validTime"] + mocked_data = pd.read_csv("tests/fixtures/mesan/multipoint_data.csv", index_col=0) + + return ( + mocked_response, + mocked_approved_time, + mocked_reference_time, + mocked_valid_time, + mocked_data, + ) + class TestUnitMesan: """Unit tests for Mesan class.""" - def test_unit_mesan_init(self): + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + def test_unit_mesan_init(self, mock_get_parameters): """Unit test for Mesan init method.""" client = Mesan() assert client._category == "mesan2g" assert client._version == 1 - assert client.latitude is None - assert client.longitude is None - assert client.status is None - assert client.header is None - assert client.base_url == BASE_URL - assert client.url is None + assert client._base_url == BASE_URL + assert client._parameters == MOCK_MESAN_PARAMETERS + + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + def test_unit_mesan_parameter_descriptions(self, mock_get_data): + """Unit test for Mesan parameter_descriptions property.""" + client = Mesan() + + assert client.parameter_descriptions == MESAN_PARAMETER_DESCRIPTIONS @patch( - "smhi.mesan.Mesan._get_data", return_value=({"approvedTime": None}, None, None) + "smhi.mesan.Mesan._get_data", + return_value=( + MOCK_MESAN_PARAMETERS.model_dump(by_alias=True), + MOCK_MESAN_PARAMETERS.headers, + MOCK_MESAN_PARAMETERS.status, + ), ) - def test_unit_mesan_approved_time(self, mock_get_data): - """Unit test for Mesan approved_time property. + def test_unit_mesan_parameters(self, mock_get_data): + """Unit test for Mesan parameters property.""" + client = Mesan() + + mock_get_data.assert_called_once_with(BASE_URL + "parameter.json") + assert client.parameters == MOCK_MESAN_PARAMETERS - Args: - mock_get_data: mock _get_data method - """ + @patch( + "smhi.mesan.Mesan._get_data", + return_value=( + {"approvedTime": "1", "referenceTime": "2"}, + {"head": "head"}, + 200, + ), + ) + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + def test_unit_mesan_approved_time(self, mock_get_parameters, mock_get_data): + """Unit test for Mesan approved_time property.""" client = Mesan() data = client.approved_time - mock_get_data.assert_called_once_with(BASE_URL + "approvedtime.json") - assert data == mock_get_data.return_value[0]["approvedTime"] - @patch("smhi.mesan.Mesan._get_data", return_value=({"validTime": None}, None, None)) - def test_unit_mesan_valid_time(self, mock_get_data): - """Unit test for Mesan valid_time property. + mock_get_data.assert_called_once_with(BASE_URL + "approvedtime.json") + assert data.approved_time == mock_get_data.return_value[0]["approvedTime"] + assert data.reference_time == mock_get_data.return_value[0]["referenceTime"] + assert data.headers == mock_get_data.return_value[1] + assert data.status == mock_get_data.return_value[2] - Args: - mock_get_data: mock _get_data method - """ + @patch( + "smhi.mesan.Mesan._get_data", + return_value=({"validTime": ["1"]}, {"head": "head"}, 200), + ) + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + def test_unit_mesan_valid_time(self, mock_get_parameters, mock_get_data): + """Unit test for Mesan valid_time property.""" client = Mesan() data = client.valid_time + mock_get_data.assert_called_once_with(BASE_URL + "validtime.json") - assert data == mock_get_data.return_value[0]["validTime"] + assert data.valid_time == mock_get_data.return_value[0]["validTime"] + assert data.headers == mock_get_data.return_value[1] + assert data.status == mock_get_data.return_value[2] @patch( "smhi.mesan.Mesan._get_data", - return_value=({"type": "Polygon", "coordinates": None}, None, None), + return_value=( + {"type": "Polygon", "coordinates": [[[1.0, 2.0]]]}, + {"head": "head"}, + 200, + ), ) - def test_unit_mesan_geo_polygon(self, mock_get_data): - """Unit test for Mesan geo_polygon property. - - Args: - mock_get_data: mock _get_data method - """ + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + def test_unit_mesan_geo_polygon(self, mock_get_parameters, mock_get_data): + """Unit test for Mesan geo_polygon property.""" client = Mesan() data = client.geo_polygon + mock_get_data.assert_called_once_with(BASE_URL + "geotype/polygon.json") - assert data == mock_get_data.return_value[0]["coordinates"] + assert data.coordinates == mock_get_data.return_value[0]["coordinates"] + assert data.type_ == mock_get_data.return_value[0]["type"] + assert data.headers == mock_get_data.return_value[1] + assert data.status == mock_get_data.return_value[2] - @pytest.mark.parametrize("downsample", [(0), (2), (20), (21)]) + @pytest.mark.parametrize( + "downsample, bounded_downsample", [(0, 1), (2, 2), (20, 20), (21, 20)] + ) @patch( "smhi.mesan.Mesan._get_data", - return_value=({"type": "MultiPoint", "coordinates": None}, None, None), + return_value=( + {"type": "MultiPoint", "coordinates": [[[1.0, 2.0]]]}, + {"head": "head"}, + 200, + ), ) - def test_unit_mesan_get_geo_multipoint(self, mock_get_data, downsample): - """Unit test for Mesan get_geo_multipoint method. - - Args: - mock_get_data: mock _get_data method - downsample: downsample parameter - """ + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + def test_unit_mesan_get_geo_multipoint( + self, mock_get_parameters, mock_get_data, downsample, bounded_downsample + ): + """Unit test for Mesan get_geo_multipoint method.""" client = Mesan() data = client.get_geo_multipoint(downsample) - if downsample < 1: - mock_get_data.assert_called_once_with(BASE_URL + "geotype/multipoint.json") - elif downsample > 20: - mock_get_data.assert_called_once_with( - BASE_URL + "geotype/multipoint.json?downsample=20" - ) - else: - mock_get_data.assert_called_once_with( - BASE_URL - + "geotype/multipoint.json?downsample={downsample}".format( - downsample=downsample - ) - ) - - assert data == mock_get_data.return_value[0]["coordinates"] - - @patch("smhi.mesan.Mesan._get_data", return_value=({"parameter": None}, None, None)) - def test_unit_mesan_parameters(self, mock_get_data): - """Unit test for Mesan parameters property. - Args: - mock_get_data: mock _get_data method - """ - client = Mesan() - data = client.parameters - mock_get_data.assert_called_once_with(BASE_URL + "parameter.json") - assert data == mock_get_data.return_value[0]["parameter"] + mock_get_data.assert_called_once_with( + BASE_URL + f"geotype/multipoint.json?downsample={bounded_downsample}" + ) + assert data.coordinates == mock_get_data.return_value[0]["coordinates"] + assert data.type_ == mock_get_data.return_value[0]["type"] + assert data.headers == mock_get_data.return_value[1] + assert data.status == mock_get_data.return_value[2] @pytest.mark.parametrize("lat, lon", [(0, 0), (1, 1)]) - @patch("smhi.mesan.Mesan._format_data_point", return_value="datatable") - @patch("smhi.mesan.Mesan._get_data", return_value=({"geometry": None}, None, None)) - def test_unit_mesan_get_point(self, mock_get_data, mock_format_data, lat, lon): - """Unit test for Mesan get_point method. - - Args: - mock_get_data: mock _get_data method - mock_format_data: mock of _format_data - lat: latitude - lon: longitude - """ + @patch("smhi.utils.requests.get") + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + def test_unit_mesan_get_point( + self, mock_get_parameters, mock_requests_get, lat, lon, setup_point + ): + """Unit test for Mesan get_point method.""" + ( + mock_response, + expected_approved_time, + expected_reference_time, + expected_geometry, + expected_answer, + expected_answer_info, + ) = setup_point + mock_requests_get.return_value = mock_response + client = Mesan() data = client.get_point(lat, lon) - mock_get_data.assert_called_once_with( - BASE_URL - + "geotype/point/lon/{longitude}/lat/{latitude}/data.json".format( - longitude=lon, latitude=lat - ) + + mock_requests_get.assert_called_once_with( + BASE_URL + f"geotype/point/lon/{lon}/lat/{lat}/data.json", timeout=200 ) - mock_format_data.assert_called_once_with(mock_get_data.return_value[0]) - assert mock_format_data.return_value == data + assert data.status == mock_response.status_code + assert data.headers == mock_response.headers + assert data.approved_time == expected_approved_time + assert data.reference_time == expected_reference_time + assert data.geometry == expected_geometry + assert data.level_unit == MESAN_LEVELS_UNIT + pd.testing.assert_frame_equal( + data.df.astype(float), expected_answer.astype(float) + ) + pd.testing.assert_frame_equal(data.df_info, expected_answer_info) @pytest.mark.parametrize( - "validtime, parameter, leveltype, level, downsample", [(0, 0, 0, 0, 0)] - ) - @patch("smhi.mesan.Mesan._format_data_multipoint", return_value="data") - @patch( - "smhi.mesan.Mesan._get_data", - return_value=({"approvedTime": "this_time"}, None, None), + "validtime, parameter, leveltype, level, geo, downsample", + [("2024-03-31T06", "t", "hl", 2, True, 1)], ) + @patch("smhi.utils.requests.get") + @patch("smhi.mesan.Mesan._get_parameters", return_value=MOCK_MESAN_PARAMETERS) + @patch("smhi.mesan.arrow.now", return_value=arrow.get("2024-03-31T07:14:10Z")) def test_unit_mesan_get_multipoint( self, - mock_get_data, - mock_format_data_multipoint, + mock_arrow_now, + mock_get_parameters, + mock_requests_get, validtime, parameter, leveltype, level, + geo, downsample, + setup_multipoint, ): - """Unit test for Mesan get_multipoint method. - - Args: - mock_get_data: mock _get_data method - mock_format_data_multipoint: mock _format_data_multipoint method - validtime: valid time, - parameter: parameter, - leveltype: level type, - level: level, - downsample: downsample, - """ + """Unit test for Mesan get_multipoint method.""" + ( + mock_response, + expected_approved_time, + expected_reference_time, + expected_valid_time, + expected_answer, + ) = setup_multipoint + mock_requests_get.return_value = mock_response + client = Mesan() - data = client.get_multipoint(validtime, parameter, leveltype, level, downsample) - validtime = arrow.get(validtime).format("YYYYMMDDThhmmss") + "Z" - mock_get_data.assert_called_once_with( - BASE_URL - + "geotype/multipoint/" - + "validtime/{YYMMDDThhmmssZ}/parameter/{p}/leveltype/".format( - YYMMDDThhmmssZ=validtime, - p=parameter, - ) - + "{lt}/level/{l}/data.json?with-geo=false&downsample={downsample}".format( - lt=leveltype, - l=level, - downsample=downsample, - ) + data = client.get_multipoint( + validtime, parameter, leveltype, level, geo, downsample ) - mock_format_data_multipoint.assert_called_once_with( - mock_get_data.return_value[0] - ) - assert mock_format_data_multipoint.return_value == data + validtime = client._format_datetime(validtime) + + assert data.status == mock_response.status_code + assert data.headers == mock_response.headers + assert data.approved_time == expected_approved_time + assert data.reference_time == expected_reference_time + assert data.valid_time == expected_valid_time + pd.testing.assert_frame_equal(data.df, expected_answer) @pytest.mark.parametrize( - "response", + "validtime, parameter, leveltype, level, geo, downsample, expected_answer", [ ( - type( - "ResponseClass", - (object,), - {"ok": False, "headers": None, "content": None}, - )() + "2024-03-31T06", + "t", + "hl", + 2, + True, + 1, + "https://opendata-download-metanalys.smhi.se/api/category/" + + "mesan2g/version/1/geotype/multipoint/" + + "validtime/20240331T060000Z/parameter/t/leveltype/" + + "hl/level/2/data.json?with-geo=true&downsample=1", ), ( - type( - "ResponseClass", - (object,), - {"ok": True, "headers": "header", "content": r"""{"data": 1}"""}, - )() + "2024-03-31T06", + "t", + "hl", + 2, + True, + 0, + "https://opendata-download-metanalys.smhi.se/api/category/" + + "mesan2g/version/1/geotype/multipoint/" + + "validtime/20240331T060000Z/parameter/t/leveltype/" + + "hl/level/2/data.json?with-geo=true&downsample=1", + ), + ( + "2024-03-31T06", + "t", + "hl", + 2, + True, + 20, + "https://opendata-download-metanalys.smhi.se/api/category/" + + "mesan2g/version/1/geotype/multipoint/" + + "validtime/20240331T060000Z/parameter/t/leveltype/" + + "hl/level/2/data.json?with-geo=true&downsample=20", + ), + ( + "2024-03-31T06", + "t", + "hl", + 2, + False, + 21, + "https://opendata-download-metanalys.smhi.se/api/category/" + + "mesan2g/version/1/geotype/multipoint/" + + "validtime/20240331T060000Z/parameter/t/leveltype/" + + "hl/level/2/data.json?with-geo=false&downsample=20", ), ], ) - @patch("smhi.mesan.json.loads") - @patch("smhi.mesan.requests.get") - def test_unit_mesan_get_data(self, mock_get, mock_loads, response): - """Unit test for Mesan _get_data method. - - Args: - mock_get: mock requests get method - mock_loads: mock json loads method - response: response object - """ + @patch("smhi.mesan.Mesan._get_parameters") + def test_build_multipoint_url( + self, + mock_get_parameters, + validtime, + parameter, + leveltype, + level, + geo, + downsample, + expected_answer, + ): + """""" client = Mesan() - mock_get.return_value = response - data, headers, status = client._get_data("url") - - mock_get.assert_called_once_with("url") - assert status is response.ok - assert headers == response.headers - if status: - assert data == json.loads(response.content) - else: - assert data is None - - def test_unit_mesan_format_data_point(self): - """Unit test for Mesan _format_data_point.""" - with open("tests/fixtures/unit_mesan_point_format.json") as f: - input_data = json.load(f) - - result = pd.read_csv( - "tests/fixtures/unit_mesan_point_format_result.csv", - parse_dates=[0], - index_col=0, - header=[0, 1], + assert ( + client._build_multipoint_url( + validtime, + parameter, + leveltype, + level, + geo, + downsample, + ) + == expected_answer ) + @pytest.mark.parametrize( + "test_time, expected_answer", + [ + ("2024-03-31T07", "20240331T070000Z"), + ("2024-03-31T06:00", "20240331T060000Z"), + ("2024-03-30T07:00:00", "20240330T070000Z"), + ("2024-03-30T060000", "20240330T060000Z"), + ("2024-03-30T060000Z", "20240330T060000Z"), + ], + ) + @patch("smhi.mesan.Mesan._get_parameters") + def test_format_datetime(self, mock_get_parameters, test_time, expected_answer): + """Unit test _format_datetime.""" client = Mesan() - data = client._format_data_point(input_data) - pd.testing.assert_frame_equal(result, data) - - def test_unit_mesan_format_data_multipoint(self): - """Unit test for Mesan _format_data_multipoint.""" - with open("tests/fixtures/unit_mesan_multipoint_format.json") as f: - input_data = json.load(f) - - result = pd.read_csv( - "tests/fixtures/unit_mesan_multipoint_format_result.csv", - parse_dates=[1, 2, 3], - index_col=0, - ) + assert client._format_datetime(test_time) == expected_answer + @pytest.mark.parametrize( + "test_time, expected_answer", + [ + ("2024-03-31T07:00:00Z", False), + ("2024-03-31T06:00:00Z", True), + ("2024-03-30T07:00:00Z", True), + ("2024-03-30T06:00:00Z", False), + ], + ) + @patch("smhi.mesan.Mesan._get_parameters") + @patch("smhi.mesan.arrow.now", return_value=arrow.get("2024-03-31T07:14:10Z")) + def test_check_valid_time( + self, mock_arrow_now, mock_get_parameters, test_time, expected_answer + ): + """Unit test _ceck_valid_time.""" client = Mesan() - data = client._format_data_multipoint(input_data) - pd.testing.assert_frame_equal(result, data) + assert client._check_valid_time(test_time) is expected_answer diff --git a/tests/unit/test_unit_metfcts.py b/tests/unit/test_unit_metfcts.py index 18e8a01e..fa5fc0d4 100644 --- a/tests/unit/test_unit_metfcts.py +++ b/tests/unit/test_unit_metfcts.py @@ -1,5 +1,9 @@ """SMHI Metfcts v1 unit tests.""" +from unittest.mock import patch + +import arrow +import pytest from smhi.metfcts import Metfcts BASE_URL = ( @@ -10,15 +14,20 @@ class TestUnitMetfcts: """Unit tests for Metfcts class.""" - def test_unit_metfcts_init(self): - """Unit test for Metfcts init method.""" + @pytest.mark.parametrize( + "test_time, expected_answer", + [ + ("2024-03-31T14:00:00Z", False), + ("2024-03-31T15:00:00Z", True), + ("2024-04-10T00:00:00Z", True), + ("2024-04-11T01:00:00Z", False), + ], + ) + @patch("smhi.metfcts.Metfcts._get_parameters") + @patch("smhi.metfcts.arrow.now", return_value=arrow.get("2024-03-31T15:14:10Z")) + def test_check_valid_time( + self, mock_arrow_now, mock_get_parameters, test_time, expected_answer + ): + """Unit test _ceck_valid_time.""" client = Metfcts() - - assert client._category == "pmp3g" - assert client._version == 2 - assert client.latitude is None - assert client.longitude is None - assert client.status is None - assert client.header is None - assert client.base_url == BASE_URL - assert client.url is None + assert client._check_valid_time(test_time) is expected_answer