"""EPC-API + geospatial adapter for the ComparableProperties port (ADR-0031). Assembles a postcode's candidate cohort: the EPC search lists the postcode's lodged certs, each is fetched + mapped to `EpcPropertyData`, and the certs' UPRNs are resolved to coordinates in one batched geospatial read (closely-numbered UPRNs share a partition). Register metadata the cert itself doesn't carry (address, registration date) is threaded off the search row. """ from __future__ import annotations from datetime import date from typing import Optional, Protocol from datatypes.epc.domain.epc_property_data import EpcPropertyData from datatypes.epc.search.epc_search_result import EpcSearchResult from domain.epc_prediction.comparable_properties import ComparableProperty from domain.geospatial.coordinates import Coordinates from repositories.comparable_properties.comparable_properties_repository import ( ComparablePropertiesRepository, ) class CohortEpcClient(Protocol): """The slice of the EPC-API client the cohort fetch needs (e.g. `EpcClientService`).""" def search_by_postcode(self, postcode: str) -> list[EpcSearchResult]: ... def get_by_certificate_number(self, cert_num: str) -> EpcPropertyData: ... class CohortGeospatial(Protocol): """The geospatial slice the cohort fetch needs — batch UPRN→coordinate.""" def coordinates_for_uprns( self, uprns: list[int] ) -> dict[int, Coordinates]: ... class EpcComparablePropertiesRepository(ComparablePropertiesRepository): def __init__( self, epc_client: CohortEpcClient, geospatial: CohortGeospatial ) -> None: self._epc_client = epc_client self._geospatial = geospatial def candidates_for(self, postcode: str) -> list[ComparableProperty]: results: list[EpcSearchResult] = self._epc_client.search_by_postcode( postcode ) uprns: list[int] = [r.uprn for r in results if r.uprn is not None] coordinates: dict[int, Coordinates] = self._geospatial.coordinates_for_uprns( uprns ) return [self._comparable(result, coordinates) for result in results] def _comparable( self, result: EpcSearchResult, coordinates: dict[int, Coordinates] ) -> ComparableProperty: epc: EpcPropertyData = self._epc_client.get_by_certificate_number( result.certificate_number ) resolved: Optional[Coordinates] = ( coordinates.get(result.uprn) if result.uprn is not None else None ) return ComparableProperty( epc=epc, certificate_number=result.certificate_number, address=result.address_line_1, registration_date=_parse_date(result.registration_date), coordinates=resolved, ) def _parse_date(value: str) -> Optional[date]: """The register's ISO registration date, or None when unparseable (the predictor falls back to an unweighted recency).""" try: return date.fromisoformat(value[:10]) except ValueError: return None