mirror of
https://github.com/Hestia-Homes/Model.git
synced 2026-07-12 13:29:04 +00:00
Adds GeospatialRepository.coordinates_for_uprns(uprns) -> dict — a batch coordinate lookup returning only covered UPRNs. The S3 adapter overrides it to read the meta once, group UPRNs by their covering partition, and read each partition once for all the UPRNs it covers; co-located (closely-numbered) UPRNs share a partition, so an EPC Prediction cohort is typically one or two reads instead of one per neighbour. Default port impl is a per-UPRN loop. Feeds the EPC Prediction geo-proximity work: a cohort's UPRNs resolve to coordinates in a couple of reads (validated at corpus scale: 170 partition reads for 2683 UPRNs). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
190 lines
6.4 KiB
Python
190 lines
6.4 KiB
Python
"""GeospatialRepo resolves a Property's coordinates from the OS Open-UPRN data.
|
|
|
|
A reference-data lookup, not a Fetcher (ADR-0011): no live OS API call. The
|
|
adapter reads the partitioned Open-UPRN parquet via an injected reader, so the
|
|
test exercises the partition lookup + filter against real fixture parquets with
|
|
no network.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Callable
|
|
from pathlib import Path
|
|
|
|
import pandas as pd
|
|
|
|
from typing import Optional
|
|
|
|
from domain.geospatial.coordinates import Coordinates
|
|
from domain.geospatial.planning_restrictions import PlanningRestrictions
|
|
from domain.geospatial.spatial_reference import SpatialReference
|
|
from repositories.geospatial.geospatial_s3_repository import GeospatialS3Repository
|
|
|
|
|
|
def _reader(base: Path) -> Callable[[str], pd.DataFrame]:
|
|
def read(key: str) -> pd.DataFrame:
|
|
return pd.read_parquet(base / key)
|
|
|
|
return read
|
|
|
|
|
|
def _write_open_uprn(base: Path) -> None:
|
|
spatial = base / "spatial"
|
|
spatial.mkdir(parents=True, exist_ok=True)
|
|
pd.DataFrame(
|
|
{"lower": [0], "upper": [100000], "filenames": ["0_100000.parquet"]}
|
|
).to_parquet(spatial / "filename_meta.parquet")
|
|
pd.DataFrame(
|
|
{
|
|
"UPRN": [12345, 12346],
|
|
"LATITUDE": [51.5074, 51.6000],
|
|
"LONGITUDE": [-0.1278, -0.2000],
|
|
# Planning flags co-located with the coordinates in the partition
|
|
# (legacy column names — confirm exact names in the S3 deep-dive).
|
|
"conservation_status": [True, False],
|
|
"is_listed_building": [False, True],
|
|
"is_heritage_building": [False, False],
|
|
}
|
|
).to_parquet(spatial / "0_100000.parquet")
|
|
|
|
|
|
def test_coordinates_for_returns_lon_lat(tmp_path: Path) -> None:
|
|
# Arrange
|
|
_write_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act
|
|
coords = repo.coordinates_for(12345)
|
|
|
|
# Assert
|
|
assert coords == Coordinates(longitude=-0.1278, latitude=51.5074)
|
|
|
|
|
|
def test_coordinates_for_returns_none_when_uprn_absent(tmp_path: Path) -> None:
|
|
# Arrange
|
|
_write_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act / Assert — uprn inside the partition range but not present in the data
|
|
assert repo.coordinates_for(99999) is None
|
|
|
|
|
|
def test_coordinates_for_returns_none_when_no_partition_covers_uprn(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
# Arrange
|
|
_write_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act / Assert — uprn beyond every partition's range
|
|
assert repo.coordinates_for(500000) is None
|
|
|
|
|
|
def test_planning_restrictions_for_reads_the_co_located_flags(tmp_path: Path) -> None:
|
|
# Arrange — same partition, planning flags alongside the coordinates.
|
|
_write_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act
|
|
restrictions = repo.planning_restrictions_for(12345)
|
|
|
|
# Assert — the three flags come back as the Property's PlanningRestrictions.
|
|
assert restrictions == PlanningRestrictions(
|
|
in_conservation_area=True, is_listed=False, is_heritage=False
|
|
)
|
|
|
|
|
|
def test_planning_restrictions_for_returns_none_when_uprn_absent(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
# Arrange
|
|
_write_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act / Assert
|
|
assert repo.planning_restrictions_for(99999) is None
|
|
|
|
|
|
def test_spatial_for_returns_coordinates_and_restrictions_together(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
# Arrange — one partition row carries the coordinates and the planning flags.
|
|
_write_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act — a single reference lookup yields both, so Ingestion reads the row once.
|
|
reference: Optional[SpatialReference] = repo.spatial_for(12346)
|
|
|
|
# Assert
|
|
assert reference == SpatialReference(
|
|
coordinates=Coordinates(longitude=-0.2000, latitude=51.6000),
|
|
restrictions=PlanningRestrictions(
|
|
in_conservation_area=False, is_listed=True, is_heritage=False
|
|
),
|
|
)
|
|
|
|
|
|
def test_spatial_for_returns_none_when_uprn_absent(tmp_path: Path) -> None:
|
|
# Arrange
|
|
_write_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act / Assert
|
|
assert repo.spatial_for(99999) is None
|
|
|
|
|
|
def _write_two_partition_open_uprn(base: Path) -> None:
|
|
"""Two UPRN-range partitions, so the batch lookup must span both."""
|
|
spatial = base / "spatial"
|
|
spatial.mkdir(parents=True, exist_ok=True)
|
|
pd.DataFrame(
|
|
{
|
|
"lower": [0, 100001],
|
|
"upper": [100000, 200000],
|
|
"filenames": ["0_100000.parquet", "100001_200000.parquet"],
|
|
}
|
|
).to_parquet(spatial / "filename_meta.parquet")
|
|
pd.DataFrame(
|
|
{"UPRN": [10, 11], "LATITUDE": [51.0, 51.1], "LONGITUDE": [-1.0, -1.1]}
|
|
).to_parquet(spatial / "0_100000.parquet")
|
|
pd.DataFrame(
|
|
{"UPRN": [150000], "LATITUDE": [52.0], "LONGITUDE": [-2.0]}
|
|
).to_parquet(spatial / "100001_200000.parquet")
|
|
|
|
|
|
def test_coordinates_for_uprns_resolves_a_batch_across_partitions(
|
|
tmp_path: Path,
|
|
) -> None:
|
|
# Arrange — UPRNs spanning two partitions, plus one absent and one off-scale.
|
|
_write_two_partition_open_uprn(tmp_path)
|
|
repo = GeospatialS3Repository(_reader(tmp_path))
|
|
|
|
# Act
|
|
resolved = repo.coordinates_for_uprns([10, 11, 150000, 99999, 500000])
|
|
|
|
# Assert — present UPRNs resolved; absent (99999) and uncovered (500000) omitted.
|
|
assert resolved == {
|
|
10: Coordinates(longitude=-1.0, latitude=51.0),
|
|
11: Coordinates(longitude=-1.1, latitude=51.1),
|
|
150000: Coordinates(longitude=-2.0, latitude=52.0),
|
|
}
|
|
|
|
|
|
def test_coordinates_for_uprns_reads_each_partition_once(tmp_path: Path) -> None:
|
|
# Arrange — count reads so co-located UPRNs don't re-read their partition.
|
|
_write_two_partition_open_uprn(tmp_path)
|
|
reads: list[str] = []
|
|
|
|
def counting_reader(key: str) -> pd.DataFrame:
|
|
reads.append(key)
|
|
return pd.read_parquet(tmp_path / key)
|
|
|
|
repo = GeospatialS3Repository(counting_reader)
|
|
|
|
# Act — two UPRNs share partition 0; one is in partition 1.
|
|
repo.coordinates_for_uprns([10, 11, 150000])
|
|
|
|
# Assert — the meta once + each of the two partitions once (3 reads, not 4).
|
|
assert reads.count("spatial/0_100000.parquet") == 1
|
|
assert reads.count("spatial/100001_200000.parquet") == 1
|
|
assert reads.count("spatial/filename_meta.parquet") == 1
|