Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 51 additions & 9 deletions harvester/utils/general_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -1330,15 +1330,9 @@ def _get_geo_lookup_interface():
return None


def _unwrap_location(input_value):
"""Extract the geometry-bearing value from a DCAT-US 3.0 Location object.

v3.0 `spatial` is a Location object or a list of them; v1.1 is a plain
string. A bare {type, coordinates} GeoJSON dict is passed through.
"""

if isinstance(input_value, list):
input_value = next((item for item in input_value if item), None)
def _unwrap_single_location(input_value):
"""Resolve a single (non-array) spatial value to its geometry-bearing
value, or None if it has nothing usable."""

if (
isinstance(input_value, dict)
Expand All @@ -1357,6 +1351,54 @@ def _unwrap_location(input_value):
return input_value


def _extract_pref_label(input_value):
"""Return a Location's prefLabel as a plain string, or None if absent,
blank, or not a dict.

Consulted by _unwrap_location only when nothing in the whole input has
usable geometry - real geometry always outranks a named-place fallback.
(Full hierarchy is geojson > delimited coords > named location; the
delimited-coords tier isn't implemented yet - translate_spatial's
existing validate_geojson -> get_geo_from_string -> munge_spatial order
is unchanged by this.)
"""

if not isinstance(input_value, dict):
return None
pref_label = input_value.get("prefLabel")
if isinstance(pref_label, str) and pref_label.strip():
return pref_label
return None


def _unwrap_location(input_value):
"""Extract the geometry-bearing value from a DCAT-US 3.0 Location object.

v3.0 `spatial` is a Location object or a list of them; v1.1 is a plain
string. A bare {type, coordinates} GeoJSON dict is passed through.

Real geometry anywhere in the input always wins over a named-place
(prefLabel) fallback found anywhere else: the first element with usable
geometry short-circuits the scan immediately. Only when NO element has
any geometry do we fall back to the first prefLabel seen during that
same scan, returned as a plain string so it flows into translate_spatial's
existing string branch (and from there, its existing locations-table
lookup) instead of being discarded.
"""

items = input_value if isinstance(input_value, list) else [input_value]

pref_label_fallback = None
for item in items:
unwrapped = _unwrap_single_location(item)
if unwrapped:
return unwrapped
if pref_label_fallback is None:
pref_label_fallback = _extract_pref_label(item)

return pref_label_fallback


def translate_spatial(input_value) -> str:
"""Normalize spatial strings/dicts into GeoJSON strings when possible."""

Expand Down
82 changes: 79 additions & 3 deletions tests/unit/test_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -382,6 +382,58 @@ def test_translate_spatial_location_array_uses_first(self):
'{"type": "Point", "coordinates": [0.0, 0.0]}'
)

def test_translate_spatial_location_array_geometry_after_pref_label_skips_lookup(
self,
):
locations = [
{"@type": "Location", "prefLabel": "Nebraska"},
{"@type": "Location", "geometry": "POINT (1.0 1.0)"},
]
fake_dbi = Mock(get_geo_from_string=Mock())
with patch(
"harvester.utils.general_utils._get_geo_lookup_interface",
lambda: fake_dbi,
):
assert translate_spatial(locations) == (
'{"type": "Point", "coordinates": [1.0, 1.0]}'
)
fake_dbi.get_geo_from_string.assert_not_called()

def test_translate_spatial_location_array_falls_back_to_pref_label_db_hit(self):
locations = [
{"@type": "Location", "prefLabel": "Nebraska"},
{"@type": "Location", "altLabel": "NE"},
]
fake_dbi = Mock(
get_geo_from_string=Mock(
return_value='{"type": "Point", "coordinates": [-99.9018, 41.4925]}'
)
)
with patch(
"harvester.utils.general_utils._get_geo_lookup_interface",
lambda: fake_dbi,
):
assert translate_spatial(locations) == (
'{"type": "Point", "coordinates": [-99.9018, 41.4925]}'
)
fake_dbi.get_geo_from_string.assert_called_once_with("Nebraska")

def test_translate_spatial_location_array_falls_back_to_pref_label_db_miss(self):
locations = [
{"@type": "Location", "prefLabel": "Nebraska"},
Comment thread
cody-seibert-gsa marked this conversation as resolved.
{"@type": "Location", "altLabel": "NE"},
]
fake_dbi = Mock(get_geo_from_string=Mock(return_value=None))
with patch(
"harvester.utils.general_utils._get_geo_lookup_interface",
lambda: fake_dbi,
):
assert translate_spatial(locations) == ""
fake_dbi.get_geo_from_string.assert_called_once_with("Nebraska")
fake_dbi.get_geo_from_string.reset_mock()
assert translate_spatial_to_geojson(locations) is None
fake_dbi.get_geo_from_string.assert_called_once_with("Nebraska")

def test_translate_spatial_location_falls_back_to_bbox(self):
location = {
"@type": "Location",
Expand Down Expand Up @@ -410,10 +462,34 @@ def test_translate_spatial_location_falls_back_to_centroid(self):
'{"type": "Point", "coordinates": [-77.0369, 38.9072]}'
)

def test_translate_spatial_location_with_no_geometry_fields(self):
def test_translate_spatial_location_pref_label_only_db_hit(self):
location = {"@type": "Location", "prefLabel": "Washington, D.C."}
fake_dbi = Mock(
get_geo_from_string=Mock(
return_value='{"type": "Point", "coordinates": [-77.0369, 38.9072]}'
)
)
with patch(
"harvester.utils.general_utils._get_geo_lookup_interface",
lambda: fake_dbi,
):
assert translate_spatial(location) == (
'{"type": "Point", "coordinates": [-77.0369, 38.9072]}'
)
fake_dbi.get_geo_from_string.assert_called_once_with("Washington, D.C.")

def test_translate_spatial_location_pref_label_only_db_miss(self):
location = {"@type": "Location", "prefLabel": "Washington, D.C."}
assert translate_spatial(location) == ""
assert translate_spatial_to_geojson(location) is None
fake_dbi = Mock(get_geo_from_string=Mock(return_value=None))
with patch(
"harvester.utils.general_utils._get_geo_lookup_interface",
lambda: fake_dbi,
):
assert translate_spatial(location) == ""
fake_dbi.get_geo_from_string.assert_called_once_with("Washington, D.C.")
fake_dbi.get_geo_from_string.reset_mock()
assert translate_spatial_to_geojson(location) is None
fake_dbi.get_geo_from_string.assert_called_once_with("Washington, D.C.")

def test_translate_spatial_location_input_unchanged(self):
location = {
Expand Down
Loading