mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Two review points from @dancafc: 1) Rename the `Comparable` dataclass → `ComparableProperty` (it models one comparable *property*; the collection stays `ComparableProperties`). Applied across domain, repositories, orchestration, harness, scripts, and tests with a word-boundary rename so `ComparableProperties` is untouched. 2) Move `PredictionTarget` out of comparable_properties.py into prediction_target.py (where `PredictionTargetAttributes` + `build_prediction_target` already live). comparable_properties.py now imports it; no import cycle (prediction_target no longer depends on comparable_properties). Importers updated. 92 tests pass across the touched suites; pyright strict clean. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
3 KiB
Python
82 lines
3 KiB
Python
"""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
|