From 35a9679220687f8984dfe10db25170a0ba8ef9d1 Mon Sep 17 00:00:00 2001 From: Khalim Conn-Kowlessar Date: Mon, 19 Jun 2023 19:42:08 +0100 Subject: [PATCH] Added property tests --- model_data/BoreholeClient.py | 18 +++++ model_data/EpcClean.py | 4 +- model_data/Property.py | 41 +++++++++++ model_data/app.py | 20 ++---- model_data/tests/test_property.py | 111 ++++++++++++++++++++++++++++-- 5 files changed, 172 insertions(+), 22 deletions(-) diff --git a/model_data/BoreholeClient.py b/model_data/BoreholeClient.py index 9e669d86..f268d6a7 100644 --- a/model_data/BoreholeClient.py +++ b/model_data/BoreholeClient.py @@ -54,3 +54,21 @@ class BoreholeClient: distance_m = math.sqrt((x2_bng - x1_bng) ** 2 + (y2_bng - y1_bng) ** 2) distance_km = distance_m / 1000 # convert meters to kilometers return distance_m, distance_km + + # EXAMPLE + # There are ~1.4 million entries in this dataset and so we firstly want to reduce the number of + # entries in here if possible before we produce any form of comparison between our properties, to infer + # the distance from the property to the nearest borehole + + # Let's take a sample + # borehold_compare_to = borehole_client.data[0] + # property = input_properties[0] + # + # # for each property, find the nearest borehole + # # This is just an example, looking at the distance from a property to a borehole + # dist_m, dist_km = borehole_client.distance_between_bng_coords( + # x1_bng=property.coordinates["x_coordinate"], + # y1_bng=property.coordinates["y_coordinate"], + # x2_bng=borehold_compare_to["X"], + # y2_bng=borehold_compare_to["Y"], + # ) diff --git a/model_data/EpcClean.py b/model_data/EpcClean.py index 6b28591f..03cc3a4e 100644 --- a/model_data/EpcClean.py +++ b/model_data/EpcClean.py @@ -21,7 +21,7 @@ class EpcClean: "hotwater-description", "main-fuel", "mainheat-description", - "main-heating-controls", + "mainheatcont-description", "roof-description", "walls-description", "windows-description", @@ -50,7 +50,7 @@ class EpcClean: self.clean_wrapper(field="hotwater-description", cleaning_cls=HotWaterAttributes) self.clean_wrapper(field="main-fuel", cleaning_cls=MainFuelAttributes) self.clean_wrapper(field="mainheat-description", cleaning_cls=MainHeatAttributes) - self.clean_wrapper(field="main-heating-controls", cleaning_cls=MainheatControlAttributes) + self.clean_wrapper(field="mainheatcont-description", cleaning_cls=MainheatControlAttributes) self.clean_wrapper(field="roof-description", cleaning_cls=RoofAttributes) self.clean_wrapper(field="walls-description", cleaning_cls=WallAttributes) self.clean_wrapper(field="windows-description", cleaning_cls=WindowAttributes) diff --git a/model_data/Property.py b/model_data/Property.py index 55c5ac90..0ff448c4 100644 --- a/model_data/Property.py +++ b/model_data/Property.py @@ -1,9 +1,30 @@ from epc_api.client import EpcClient from model_data.config import EPC_AUTH_TOKEN from model_data.OpenUprnClient import OpenUprnClient +from model_data.EpcClean import EpcClean class Property: + ATTRIBUTE_MAP = { + "floor-description": "floor", + "hotwater-description": "hotwater", + "main-fuel": "main_fuel", + "mainheat-description": "main_heating", + "mainheatcont-description": "main_heating_controls", + "roof-description": "roof", + "walls-description": "walls", + "windows-description": "windows", + } + + floor = None + hotwater = None + main_fuel = None + main_heating = None + main_heating_controls = None + roof = None + walls = None + windows = None + coordinates = None def __init__(self, postcode, address1, epc_client=None, data=None): @@ -56,3 +77,23 @@ class Property: ) self.coordinates = {key.lower(): value for key, value in self.coordinates.items()} + + def get_components(self, cleaner: EpcClean): + """ + Given the cleaning that has been performed, we'll use this to identify the property + components, from roof to walls to windows, heating and hot water + :param cleaner: + :return: + """ + + if not cleaner.cleaned: + raise ValueError("Cleaner does not contain cleaned data") + + for description, attribute in cleaner.cleaned.items(): + + attributes = [ + x for x in cleaner.cleaned[description] if x["original_description"] == self.data[description] + ] + if len(attributes) != 1: + raise ValueError("Either No attributes or multiple found for %s" % description) + setattr(self, self.ATTRIBUTE_MAP[description], attributes[0]) diff --git a/model_data/app.py b/model_data/app.py index f318a13f..29ca8cab 100644 --- a/model_data/app.py +++ b/model_data/app.py @@ -106,19 +106,7 @@ def handler(): ) borehole_client.read() - # There are ~1.4 million entries in this dataset and so we firstly want to reduce the number of - # entries in here if possible before we produce any form of comparison between our properties, to infer - # the distance from the property to the nearest borehole - - # Let's take a sample - borehold_compare_to = borehole_client.data[0] - property = input_properties[0] - - # for each property, find the nearest borehole - # This is just an example, looking at the distance from a property to a borehole - dist_m, dist_km = borehole_client.distance_between_bng_coords( - x1_bng=property.coordinates["x_coordinate"], - y1_bng=property.coordinates["y_coordinate"], - x2_bng=borehold_compare_to["X"], - y2_bng=borehold_compare_to["Y"], - ) + # Now, for our input properties, we need to identify the components of the building, based + # on the cleaning we've done + for p in input_properties: + p.get_components(cleaner) diff --git a/model_data/tests/test_property.py b/model_data/tests/test_property.py index 20b78963..5eef4896 100644 --- a/model_data/tests/test_property.py +++ b/model_data/tests/test_property.py @@ -1,7 +1,10 @@ import pytest +import pandas as pd from unittest.mock import Mock from epc_api.client import EpcClient from model_data.Property import Property +from model_data.OpenUprnClient import OpenUprnClient +from model_data.EpcClean import EpcClean # Define some test data mock_epc_response = { @@ -9,21 +12,54 @@ mock_epc_response = { { "inspection-date": "2023-06-01", "some-other-key": "some-value", - # add other keys as necessary + "roof-description": "Roof Description", + "walls-description": "Walls Description", + "windows-description": "Windows Description", + "mainheat-description": "Main Heating Description", + "hotwater-description": "Hot Water Description" }, { "inspection-date": "2023-05-01", "some-other-key": "some-other-value", - # add other keys as necessary + "roof-description": "Roof Description", + "walls-description": "Walls Description", + "windows-description": "Windows Description", + "mainheat-description": "Main Heating Description", + "hotwater-description": "Hot Water Description" } ] } # Create a mock EPC client mock_client = Mock(spec=EpcClient()) -mock_client.domestic.search.return_value = mock_epc_response +mock_client.domestic.search.return_value = mock_epc_response.copy() mock_client.auth_token = "mocked_auth_token" +# Create a mock OpenUprnClient instance +mock_open_uprn_client = Mock(spec=OpenUprnClient(path=None, uprns=[12345])) +mock_open_uprn_client.data = pd.DataFrame( + [ + {"UPRN": 12345, "longitude": 1.2345, "latitude": 2.3456}, + {"UPRN": 12346, "longitude": 3.4567, "latitude": 4.5678} + ] +) + +# Create a mock EpcClean instance +mock_cleaner = Mock(spec=EpcClean(data=[ + {"roof-description": "Roof Description"}, + {"walls-description": "Walls Description"}, + {"windows-description": "Windows Description"}, + {"mainheat-description": "Main Heating Description"}, + {"hotwater-description": "Hot Water Description"} +])) +mock_cleaner.cleaned = { + "roof-description": [{"original_description": "Roof Description"}], + "walls-description": [{"original_description": "Walls Description"}], + "windows-description": [{"original_description": "Windows Description"}], + "mainheat-description": [{"original_description": "Main Heating Description"}], + "hotwater-description": [{"original_description": "Hot Water Description"}] +} + class TestProperty: @pytest.fixture @@ -53,10 +89,77 @@ class TestProperty: def test_search_address_epc_multiple_results(self, property_instance): # Modify the mock response to return two results with the same date - mock_epc_response["rows"].append({ + mock_client.domestic.search.return_value["rows"].append({ "inspection-date": "2023-06-01", "some-other-key": "duplicate-date" }) with pytest.raises(Exception, match="More than one result found for this address - investigate me"): property_instance.search_address_epc() + + # Reset the change + mock_client.domestic.search.return_value["rows"].pop(-1) + assert len(mock_client.domestic.search.return_value["rows"]) == 1 + + def test_get_coordinates(self, property_instance): + # Set up the mock OpenUprnClient + property_instance.data = {"uprn": 12345} + property_instance.get_coordinates(mock_open_uprn_client) + + # Verify that the coordinates are set correctly + assert property_instance.coordinates == { + "uprn": 12345, + "longitude": 1.2345, + "latitude": 2.3456 + } + + def test_get_coordinates_without_open_uprn_data(self, property_instance): + # Modify the mock OpenUprnClient to not have read any data + mock_open_uprn_client.data = None + + # Verify that ValueError is raised when OpenUprnClient data is None + with pytest.raises(ValueError, match="OpenUprnClient has not read data"): + property_instance.get_coordinates(mock_open_uprn_client) + + def test_get_components(self, property_instance): + property_instance.search_address_epc() + property_instance.get_components(mock_cleaner) + + # Verify that the components are set correctly + assert property_instance.roof == {"original_description": "Roof Description"} + assert property_instance.walls == {"original_description": "Walls Description"} + assert property_instance.windows == {"original_description": "Windows Description"} + assert property_instance.main_heating == {"original_description": "Main Heating Description"} + assert property_instance.hotwater == {"original_description": "Hot Water Description"} + + def test_get_components_without_cleaned_data(self, property_instance): + # Modify the mock EpcClean to not have cleaned data + mock_cleaner.cleaned = {} + + # Verify that ValueError is raised when EpcClean doesn't contain cleaned data + with pytest.raises(ValueError, match="Cleaner does not contain cleaned data"): + property_instance.get_components(mock_cleaner) + + def test_get_components_no_attributes(self, property_instance): + # Modify the mock cleaner to have no attributes for a specific description + mock_cleaner.cleaned = { + "roof-description": [] + } + + # Verify that ValueError is raised when no attributes are found + with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"): + property_instance.get_components(mock_cleaner) + + def test_get_components_multiple_attributes(self, property_instance): + # This shouldn't happen - it would mean a cleaning error + property_instance.search_address_epc() + mock_cleaner.cleaned = { + "roof-description": [ + {"original_description": "Roof Description"}, + {"original_description": "Roof Description"} + ] + } + + # Verify that ValueError is raised when multiple attributes are found + with pytest.raises(ValueError, match="Either No attributes or multiple found for roof-description"): + property_instance.get_components(mock_cleaner)