From 8635e2a1aaf2072d4fc09e7fe7bc0de8984b71ea Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Tue, 12 May 2026 10:08:00 +0000 Subject: [PATCH] change file name of epc client service --- backend/address2UPRN/main.py | 2 +- backend/epc_client/__init__.py | 2 +- backend/epc_client/client.py | 99 ------------------------- backend/epc_client/tests/conftest.py | 2 +- backend/epc_client/tests/test_client.py | 30 ++++++-- backend/utils/epc_address_match.py | 2 +- 6 files changed, 28 insertions(+), 109 deletions(-) delete mode 100644 backend/epc_client/client.py diff --git a/backend/address2UPRN/main.py b/backend/address2UPRN/main.py index 8832e157..7e0baeaa 100644 --- a/backend/address2UPRN/main.py +++ b/backend/address2UPRN/main.py @@ -23,7 +23,7 @@ from backend.utils.addressMatch import ( from datatypes.epc.domain.historic_epc_matching import ( match_addresses_for_postcode, ) -from backend.epc_client.client import EpcClientService +from backend.epc_client.epc_client_service import EpcClientService from datatypes.epc.domain.historic_epc_matching import ScoredHistoricEpc logger = setup_logger() diff --git a/backend/epc_client/__init__.py b/backend/epc_client/__init__.py index ab46a266..84062592 100644 --- a/backend/epc_client/__init__.py +++ b/backend/epc_client/__init__.py @@ -1,3 +1,3 @@ -from backend.epc_client.client import EpcClientService +from backend.epc_client.epc_client_service import EpcClientService __all__ = ["EpcClientService"] diff --git a/backend/epc_client/client.py b/backend/epc_client/client.py deleted file mode 100644 index d00a164f..00000000 --- a/backend/epc_client/client.py +++ /dev/null @@ -1,99 +0,0 @@ -# Spec: https://raw.githubusercontent.com/communitiesuk/epb-data-warehouse/main/api/api.yml -from __future__ import annotations - -from typing import Any, Optional - -import httpx - -from backend.epc_client.exceptions import ( - EpcApiError, - EpcNotFoundError, - EpcRateLimitError, -) -from backend.epc_client._retry import call_with_retry -from datatypes.epc.domain.epc_property_data import EpcPropertyData -from datatypes.epc.domain.mapper import EpcPropertyDataMapper -from datatypes.epc.search import EpcSearchResult - - -class EpcClientService: - BASE_URL = "https://api.get-energy-performance-data.communities.gov.uk" - - def __init__(self, auth_token: str) -> None: - self._headers = { - "Authorization": f"Bearer {auth_token}", - "Accept": "application/json", - } - - def get_by_certificate_number(self, cert_num: str) -> EpcPropertyData: - raw = call_with_retry(lambda: self._fetch_certificate(cert_num)) - return EpcPropertyDataMapper.from_api_response(raw) - - def get_by_uprn(self, uprn: int) -> Optional[EpcPropertyData]: - results = call_with_retry(lambda: self._search(uprn=uprn)) - if not results: - return None - latest = max(results, key=lambda r: r.registration_date) - return self.get_by_certificate_number(latest.certificate_number) - - def search_by_postcode(self, postcode: str) -> list[EpcSearchResult]: - return call_with_retry(lambda: self._search(postcode=postcode)) - - # ------------------------------------------------------------------ - # Private helpers - # ------------------------------------------------------------------ - - def _fetch_certificate(self, cert_num: str) -> dict[str, Any]: - resp = httpx.get( - f"{self.BASE_URL}/api/certificate", - params={"certificate_number": cert_num}, - headers=self._headers, - ) - if resp.status_code == 404: - raise EpcNotFoundError(cert_num) - if resp.status_code == 429: - raise EpcRateLimitError("Rate limited by EPC API") - if not resp.is_success: - raise EpcApiError(f"EPC API error {resp.status_code}: {resp.text}") - return resp.json()["data"] - - def _search( - self, - postcode: Optional[str] = None, - uprn: Optional[int] = None, - ) -> list[EpcSearchResult]: - params: dict[str, str | int] = {} - if postcode: - params["postcode"] = postcode - if uprn is not None: - params["uprn"] = uprn - - resp = httpx.get( - f"{self.BASE_URL}/api/domestic/search", - params=params, - headers=self._headers, - ) - if resp.status_code == 404: - return [] - if resp.status_code == 429: - raise EpcRateLimitError("Rate limited by EPC API") - if not resp.is_success: - raise EpcApiError(f"EPC API error {resp.status_code}: {resp.text}") - - rows = resp.json().get("data", []) - return [self._parse_search_result(r) for r in rows] - - @staticmethod - def _parse_search_result(row: dict[str, Any]) -> EpcSearchResult: - return EpcSearchResult( - certificate_number=row["certificateNumber"], - address_line_1=row["addressLine1"], - address_line_2=row.get("addressLine2"), - address_line_3=row.get("addressLine3"), - address_line_4=row.get("addressLine4"), - postcode=row["postcode"], - post_town=row["postTown"], - uprn=row.get("uprn"), - current_energy_efficiency_band=row["currentEnergyEfficiencyBand"], - registration_date=row["registrationDate"], - ) diff --git a/backend/epc_client/tests/conftest.py b/backend/epc_client/tests/conftest.py index 2ed444af..2dab138e 100644 --- a/backend/epc_client/tests/conftest.py +++ b/backend/epc_client/tests/conftest.py @@ -2,7 +2,7 @@ import json import pathlib import pytest -from backend.epc_client.client import EpcClientService +from backend.epc_client.epc_client_service import EpcClientService SAMPLES_DIR = pathlib.Path("backend/epc_api/json_samples") diff --git a/backend/epc_client/tests/test_client.py b/backend/epc_client/tests/test_client.py index 7933f21d..849b4a25 100644 --- a/backend/epc_client/tests/test_client.py +++ b/backend/epc_client/tests/test_client.py @@ -1,7 +1,7 @@ from unittest.mock import MagicMock, patch, call import pytest -from backend.epc_client.client import EpcClientService +from backend.epc_client.epc_client_service import EpcClientService from backend.utils.epc_address_match import find_best_epc_match from datatypes.epc.search import EpcSearchResult from backend.epc_client.exceptions import EpcNotFoundError, EpcRateLimitError @@ -22,7 +22,10 @@ def _mock_response(status_code=200, json_data=None): # Test 1: get_by_certificate_number happy path # --------------------------------------------------------------------------- -def test_get_by_certificate_number_returns_epc_property_data(epc_service, rdsap_21_0_1_cert): + +def test_get_by_certificate_number_returns_epc_property_data( + epc_service, rdsap_21_0_1_cert +): cert_response = {"data": rdsap_21_0_1_cert} with patch("httpx.get", return_value=_mock_response(200, cert_response)): result = epc_service.get_by_certificate_number("CERT-001") @@ -34,6 +37,7 @@ def test_get_by_certificate_number_returns_epc_property_data(epc_service, rdsap_ # Test 2: get_by_certificate_number 404 → EpcNotFoundError # --------------------------------------------------------------------------- + def test_get_by_certificate_number_404_raises_not_found(epc_service): with patch("httpx.get", return_value=_mock_response(404)): with pytest.raises(EpcNotFoundError): @@ -44,7 +48,10 @@ def test_get_by_certificate_number_404_raises_not_found(epc_service): # Test 3: 429 retried, succeeds on 3rd attempt # --------------------------------------------------------------------------- -def test_get_by_certificate_number_retries_on_429_and_succeeds(epc_service, rdsap_21_0_1_cert): + +def test_get_by_certificate_number_retries_on_429_and_succeeds( + epc_service, rdsap_21_0_1_cert +): cert_response = {"data": rdsap_21_0_1_cert} responses = [ _mock_response(429), @@ -61,6 +68,7 @@ def test_get_by_certificate_number_retries_on_429_and_succeeds(epc_service, rdsa # Test 4: get_by_uprn empty search → None # --------------------------------------------------------------------------- + def test_get_by_uprn_returns_none_when_no_results(epc_service): with patch("httpx.get", return_value=_mock_response(200, {"data": []})): result = epc_service.get_by_uprn(100023336956) @@ -72,6 +80,7 @@ def test_get_by_uprn_returns_none_when_no_results(epc_service): # Test 5: get_by_uprn multiple results → fetches latest by registration_date # --------------------------------------------------------------------------- + def test_get_by_uprn_picks_most_recent_certificate(epc_service, rdsap_21_0_1_cert): search_rows = [ make_search_row(cert_num="CERT-OLD", registration_date="2022-01-01"), @@ -98,6 +107,7 @@ def test_get_by_uprn_picks_most_recent_certificate(epc_service, rdsap_21_0_1_cer # Test 6: search_by_postcode returns list[EpcSearchResult] # --------------------------------------------------------------------------- + def test_search_by_postcode_returns_results(epc_service): rows = [ make_search_row(cert_num="CERT-A", address_line_1="1 High Street"), @@ -116,6 +126,7 @@ def test_search_by_postcode_returns_results(epc_service): # Test 7: search_by_postcode 404 → empty list # --------------------------------------------------------------------------- + def test_search_by_postcode_404_returns_empty_list(epc_service): with patch("httpx.get", return_value=_mock_response(404)): results = epc_service.search_by_postcode("ZZ9 9ZZ") @@ -127,6 +138,7 @@ def test_search_by_postcode_404_returns_empty_list(epc_service): # Tests 8-10: find_best_epc_match — real scoring, only HTTP mocked # --------------------------------------------------------------------------- + def test_find_best_match_clear_winner_on_first_pass(epc_service, rdsap_21_0_1_cert): search_rows = [ make_search_row(cert_num="CERT-WIN", address_line_1="1 High Street"), @@ -145,7 +157,9 @@ def test_find_best_match_clear_winner_on_first_pass(epc_service, rdsap_21_0_1_ce assert isinstance(result, EpcPropertyData) -def test_find_best_match_resolves_on_second_pass_using_full_address(epc_service, rdsap_21_0_1_cert): +def test_find_best_match_resolves_on_second_pass_using_full_address( + epc_service, rdsap_21_0_1_cert +): # Both candidates share address_line_1 — round 1 is ambiguous. # Round 2 scores against full_address and picks the correct floor. search_rows = [ @@ -168,7 +182,9 @@ def test_find_best_match_resolves_on_second_pass_using_full_address(epc_service, return _mock_response(200, cert_response) with patch("httpx.get", side_effect=fake_get): - result = find_best_epc_match(epc_service, "SW1A 1AA", "1 High Street Ground Floor") + result = find_best_epc_match( + epc_service, "SW1A 1AA", "1 High Street Ground Floor" + ) assert isinstance(result, EpcPropertyData) @@ -177,6 +193,8 @@ def test_find_best_match_returns_none_when_no_good_match(epc_service): search_rows = [make_search_row(cert_num="CERT-X", address_line_1="99 Nowhere Lane")] with patch("httpx.get", return_value=_mock_response(200, {"data": search_rows})): - result = find_best_epc_match(epc_service, "SW1A 1AA", "1 Completely Different Road") + result = find_best_epc_match( + epc_service, "SW1A 1AA", "1 Completely Different Road" + ) assert result is None diff --git a/backend/utils/epc_address_match.py b/backend/utils/epc_address_match.py index f73d6d1d..0df56eca 100644 --- a/backend/utils/epc_address_match.py +++ b/backend/utils/epc_address_match.py @@ -7,7 +7,7 @@ from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.search import EpcSearchResult if TYPE_CHECKING: - from backend.epc_client.client import EpcClientService + from backend.epc_client.epc_client_service import EpcClientService _MIN_MATCH_SCORE = 0.6