From d7fd093c4f6681b5329f947b79e6e1e5933e6323 Mon Sep 17 00:00:00 2001 From: Jun-te Kim Date: Mon, 29 Jun 2026 14:51:33 +0000 Subject: [PATCH] =?UTF-8?q?Compose=20repository=20and=20matcher=20into=20s?= =?UTF-8?q?cored=20historic=20EPC=20matches=20=F0=9F=9F=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../historic_epc/historic_epc_resolver.py | 33 ++++++++++++ .../test_historic_epc_resolver.py | 53 +++++++++++++++++++ 2 files changed, 86 insertions(+) create mode 100644 repositories/historic_epc/historic_epc_resolver.py create mode 100644 tests/repositories/historic_epc/test_historic_epc_resolver.py diff --git a/repositories/historic_epc/historic_epc_resolver.py b/repositories/historic_epc/historic_epc_resolver.py new file mode 100644 index 000000000..ee6262be4 --- /dev/null +++ b/repositories/historic_epc/historic_epc_resolver.py @@ -0,0 +1,33 @@ +from __future__ import annotations + +from typing import Optional + +from datatypes.epc.domain.historic_epc_matching import ( + HistoricEpcMatches, + rank_historic_epc, +) +from repositories.historic_epc.historic_epc_repository import ( + HistoricEpcRepository, + PostcodeNotFound, +) + + +class HistoricEpcResolver: + """Resolves an address to a historic-EPC match by composing the repository + (fetch a postcode's records) with the matcher (score them against the + address). This is where ``address2uprn`` plugs onto the old-EPC backup. + """ + + def __init__(self, repo: HistoricEpcRepository) -> None: + self._repo = repo + + def match(self, user_address: str, postcode: str) -> HistoricEpcMatches: + """All of the postcode's historic EPCs scored against ``user_address``.""" + raise NotImplementedError + + def resolve_uprn( + self, user_address: str, postcode: str + ) -> Optional[tuple[str, str, float]]: + """``(uprn, matched_address, lexiscore)`` for an unambiguous rank-1 + match, else None (no data / ambiguous tie / zero score).""" + raise NotImplementedError diff --git a/tests/repositories/historic_epc/test_historic_epc_resolver.py b/tests/repositories/historic_epc/test_historic_epc_resolver.py new file mode 100644 index 000000000..a22c14b8d --- /dev/null +++ b/tests/repositories/historic_epc/test_historic_epc_resolver.py @@ -0,0 +1,53 @@ +"""HistoricEpcResolver composes the repository with the address matcher. + +Exercised against a fake in-memory repository (a dict of postcode -> records), +so the resolver's composition is tested with no S3 and no network — the matcher +and the repo each have their own tests. +""" + +from __future__ import annotations + +import dataclasses + +from datatypes.epc.domain.historic_epc import HistoricEpc +from datatypes.epc.domain.historic_epc_matching import HistoricEpcMatches +from repositories.historic_epc.historic_epc_repository import HistoricEpcRepository +from repositories.historic_epc.historic_epc_resolver import HistoricEpcResolver + + +def _hist(address: str, uprn: str) -> HistoricEpc: + fields = {f.name: "" for f in dataclasses.fields(HistoricEpc)} + fields["address"] = address + fields["uprn"] = uprn + return HistoricEpc(**fields) + + +class _FakeRepo(HistoricEpcRepository): + def __init__(self, by_postcode: dict[str, list[HistoricEpc]]) -> None: + self._by_postcode = by_postcode + + def get_for_postcode(self, postcode: str) -> list[HistoricEpc]: + key = postcode.upper().replace(" ", "") + return self._by_postcode.get(key, []) + + +def test_match_composes_repo_and_matcher_into_scored_matches(): + # Arrange + repo = _FakeRepo( + { + "AB338AL": [ + _hist("48 GORDON ROAD", "200"), + _hist("47 GORDON ROAD", "100"), + ] + } + ) + resolver = HistoricEpcResolver(repo) + + # Act + result = resolver.match("47 Gordon Road", "AB33 8AL") + + # Assert + assert isinstance(result, HistoricEpcMatches) + assert result.postcode == "AB338AL" + assert len(result.matches) == 2 + assert result.top().record.address == "47 GORDON ROAD"